Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor!(ui, llc): Attachments refactor. #1667

Merged
merged 12 commits into from
Sep 7, 2023
2 changes: 1 addition & 1 deletion packages/stream_chat/lib/src/client/channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ class Channel {
]);
}

final isImage = it.type == 'image';
final isImage = it.type == AttachmentType.image;
final cancelToken = CancelToken();
Future<SendAttachmentResponse> future;
if (isImage) {
Expand Down
74 changes: 61 additions & 13 deletions packages/stream_chat/lib/src/core/models/attachment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,25 @@ import 'package:uuid/uuid.dart';

part 'attachment.g.dart';

mixin AttachmentType {
/// Backend specified types.
static const image = 'image';
static const file = 'file';
static const giphy = 'giphy';
static const video = 'video';
static const audio = 'audio';

/// Application custom types.
static const urlPreview = 'url_preview';
}

/// The class that contains the information about an attachment
@JsonSerializable(includeIfNull: false)
class Attachment extends Equatable {
/// Constructor used for json serialization
Attachment({
String? id,
this.type,
String? type,
this.titleLink,
String? title,
this.thumbUrl,
Expand All @@ -33,26 +45,24 @@ class Attachment extends Equatable {
this.authorLink,
this.authorIcon,
this.assetUrl,
List<Action>? actions,
this.actions = const [],
this.originalWidth,
this.originalHeight,
Map<String, Object?> extraData = const {},
this.file,
UploadState? uploadState,
}) : id = id ?? const Uuid().v4(),
_type = type,
title = title ?? file?.name,
_uploadState = uploadState,
localUri = file?.path != null ? Uri.parse(file!.path!) : null,
actions = actions ?? [],
// For backwards compatibility,
// set 'file_size', 'mime_type' in [extraData].
extraData = {
...extraData,
if (file?.size != null) 'file_size': file?.size,
if (file?.mimeType != null) 'mime_type': file?.mimeType?.mimeType,
} {
this.uploadState = uploadState ??
((assetUrl != null || imageUrl != null || thumbUrl != null)
? const UploadState.success()
: const UploadState.preparing());
}
if (file?.mediaType != null) 'mime_type': file?.mediaType?.mimeType,
};

/// Create a new instance from a json
factory Attachment.fromJson(Map<String, dynamic> json) =>
Expand All @@ -69,7 +79,8 @@ class Attachment extends Equatable {

factory Attachment.fromOGAttachment(OGAttachmentResponse ogAttachment) =>
Attachment(
type: ogAttachment.type,
// If the type is not specified, we default to urlPreview.
type: ogAttachment.type ?? AttachmentType.urlPreview,
title: ogAttachment.title,
titleLink: ogAttachment.titleLink,
text: ogAttachment.text,
Expand All @@ -84,7 +95,20 @@ class Attachment extends Equatable {

///The attachment type based on the URL resource. This can be: audio,
///image or video
final String? type;
String? get type {
// If the attachment contains titleLink but is not of type giphy, we
// consider it as a urlPreview.
Comment on lines +99 to +100
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment just duplicates the code below.
Could we put the reason why we're doing this instead (why we consider it as urlPreview)? 🙂

if (_type != AttachmentType.giphy && titleLink != null) {
return AttachmentType.urlPreview;
}

return _type;
}

final String? _type;

/// The raw attachment type.
String? get rawType => _type;

///The link to which the attachment message points to.
final String? titleLink;
Expand Down Expand Up @@ -126,13 +150,27 @@ class Attachment extends Equatable {
/// Actions from a command
final List<Action>? actions;

/// The original width of the attached image.
final int? originalWidth;

/// The original height of the attached image.
final int? originalHeight;

final Uri? localUri;

/// The file present inside this attachment.
final AttachmentFile? file;

/// The current upload state of the attachment
late final UploadState uploadState;
UploadState get uploadState {
if (_uploadState case final state?) return state;

return ((assetUrl != null || imageUrl != null || thumbUrl != null)
? const UploadState.success()
: const UploadState.preparing());
}

final UploadState? _uploadState;

/// Map of custom channel extraData
final Map<String, Object?> extraData;
Expand Down Expand Up @@ -175,6 +213,8 @@ class Attachment extends Equatable {
'author_icon',
'asset_url',
'actions',
'original_width',
'original_height',
];

/// Known db specific top level fields.
Expand Down Expand Up @@ -214,6 +254,8 @@ class Attachment extends Equatable {
String? authorIcon,
String? assetUrl,
List<Action>? actions,
int? originalWidth,
int? originalHeight,
AttachmentFile? file,
UploadState? uploadState,
Map<String, Object?>? extraData,
Expand All @@ -238,6 +280,8 @@ class Attachment extends Equatable {
authorIcon: authorIcon ?? this.authorIcon,
assetUrl: assetUrl ?? this.assetUrl,
actions: actions ?? this.actions,
originalWidth: originalWidth ?? this.originalWidth,
originalHeight: originalHeight ?? this.originalHeight,
file: file ?? this.file,
uploadState: uploadState ?? this.uploadState,
extraData: extraData ?? this.extraData,
Expand All @@ -264,6 +308,8 @@ class Attachment extends Equatable {
authorIcon: other.authorIcon,
assetUrl: other.assetUrl,
actions: other.actions,
originalWidth: other.originalWidth,
originalHeight: other.originalHeight,
file: other.file,
uploadState: other.uploadState,
extraData: other.extraData,
Expand Down Expand Up @@ -291,6 +337,8 @@ class Attachment extends Equatable {
authorIcon,
assetUrl,
actions,
originalWidth,
originalHeight,
file,
uploadState,
extraData,
Expand Down
9 changes: 7 additions & 2 deletions packages/stream_chat/lib/src/core/models/attachment.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions packages/stream_chat/lib/src/core/models/attachment_file.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class AttachmentFile {
String? get extension => name?.split('.').last;

/// The mime type of this file.
MediaType? get mimeType => name?.mimeType;
MediaType? get mediaType => name?.mediaType;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a breaking change?
Should we do the deprecation cycle before replacing it with mediaType?


/// Serialize to json
Map<String, dynamic> toJson() => _$AttachmentFileToJson(this);
Expand All @@ -75,13 +75,13 @@ class AttachmentFile {
multiPartFile = MultipartFile.fromBytes(
bytes!,
filename: name,
contentType: mimeType,
contentType: mediaType,
);
} else {
multiPartFile = await MultipartFile.fromFile(
path!,
filename: name,
contentType: mimeType,
contentType: mediaType,
);
}
return multiPartFile;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import 'package:stream_chat/src/core/models/attachment.dart';

/// {@template giphy_info_type}
/// The different types of quality for a Giphy attachment.
/// {@endtemplate}
enum GiphyInfoType {
/// Original quality giphy, the largest size to load.
original('original'),

/// Lower quality with a fixed height, adjusts width according to the
/// Giphy aspect ratio. Lower size than [original].
fixedHeight('fixed_height'),

/// Still image of the [fixedHeight] giphy.
fixedHeightStill('fixed_height_still'),

/// Lower quality with a fixed height with width adjusted according to the
/// aspect ratio and played at a lower frame rate. Significantly lower size,
/// but visually less appealing.
fixedHeightDownsampled('fixed_height_downsampled');

/// {@macro giphy_info_type}
const GiphyInfoType(this.value);

/// The value of the [GiphyInfoType].
final String value;
}

/// {@template giphy_info}
/// A class that contains extra information about a Giphy attachment.
/// {@endtemplate}
class GiphyInfo {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious if need Equatable here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed. Used just to store some values.

/// {@macro giphy_info}
const GiphyInfo({
required this.url,
required this.width,
required this.height,
});

/// The url for the Giphy image.
final String url;

/// The width of the Giphy image.
final double width;

/// The height of the Giphy image.
final double height;

@override
String toString() => 'GiphyInfo{url: $url, width: $width, height: $height}';
}

/// GiphyInfo extension on [Attachment] class.
extension GiphyInfoX on Attachment {
/// Returns the [GiphyInfo] for the given [type].
GiphyInfo? giphyInfo(GiphyInfoType type) {
final giphy = extraData['giphy'] as Map<String, Object?>?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we extract 'giphy' as shared const or enum?
Is this the only place where we use hardcoded 'giphy' value?

if (giphy == null) return null;

final info = giphy[type.value] as Map<String, Object?>?;
if (info == null) return null;

return GiphyInfo(
url: info['url']! as String,
width: double.parse(info['width']! as String),
height: double.parse(info['height']! as String),
);
}
}
3 changes: 3 additions & 0 deletions packages/stream_chat/lib/src/core/models/message.dart
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ class Message extends Equatable {
/// Message custom extraData.
final Map<String, Object?> extraData;

/// True if the message is a error.
bool get isError => type == 'error';

/// True if the message is a system info.
bool get isSystem => type == 'system';

Expand Down
4 changes: 2 additions & 2 deletions packages/stream_chat/lib/src/core/util/extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ extension MapX<K, V> on Map<K?, V?> {

/// Useful extension functions for [String]
extension StringX on String {
/// returns the mime type from the passed file name.
MediaType? get mimeType {
/// returns the media type from the passed file name.
MediaType? get mediaType {
if (toLowerCase().endsWith('heic')) {
return MediaType.parse('image/heic');
} else {
Expand Down
1 change: 1 addition & 0 deletions packages/stream_chat/lib/stream_chat.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export 'src/core/http/interceptor/logging_interceptor.dart';
export 'src/core/models/action.dart';
export 'src/core/models/attachment.dart';
export 'src/core/models/attachment_file.dart';
export 'src/core/models/attachment_giphy_info.dart';
export 'src/core/models/channel_config.dart';
export 'src/core/models/channel_model.dart';
export 'src/core/models/channel_mute.dart';
Expand Down
2 changes: 1 addition & 1 deletion packages/stream_chat/test/fixtures/message_to_json.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"silent": false,
"attachments": [
{
"type": "video",
"type": "giphy",
"title_link": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif",
"title": "The Lion King Disney GIF - Find & Share on GIPHY",
"thumb_url": "https://media.giphy.com/media/5zvN79uTGfLMOVfQaA/giphy.gif",
Expand Down
2 changes: 1 addition & 1 deletion packages/stream_chat/test/src/client/client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -805,7 +805,7 @@ void main() {
emits(ConnectionStatus.disconnected),
);

await client.disconnectUser();
await client.disconnectUser(flushChatPersistence: true);

expect(client.state.currentUser, isNull);
expect(client.wsConnectionStatus, ConnectionStatus.disconnected);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ void main() {

test('should serialize to json correctly', () {
final channel = Attachment(
type: 'image',
type: 'giphy',
title: 'soo',
titleLink:
'https://giphy.com/gifs/nrkp3-dance-happy-3o7TKnCdBx5cMg0qti',
Expand All @@ -36,7 +36,7 @@ void main() {
expect(
channel.toJson(),
{
'type': 'image',
'type': 'giphy',
'title': 'soo',
'title_link':
'https://giphy.com/gifs/nrkp3-dance-happy-3o7TKnCdBx5cMg0qti',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ void main() {
'https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA',
attachments: [
Attachment.fromJson(const {
'type': 'video',
'type': 'giphy',
'author_name': 'GIPHY',
'title': 'The Lion King Disney GIF - Find \u0026 Share on GIPHY',
'title_link':
Expand Down
6 changes: 3 additions & 3 deletions packages/stream_chat/test/src/core/util/extension_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,21 @@ void main() {
group('mimeType', () {
test('should return null if `String` is not a filename', () {
const fileName = 'not-a-file-name';
final mimeType = fileName.mimeType;
final mimeType = fileName.mediaType;
expect(mimeType, isNull);
});

test('should return mimeType if string is a filename', () {
const fileName = 'dummyFileName.jpeg';
final mimeType = fileName.mimeType;
final mimeType = fileName.mediaType;
expect(mimeType, isNotNull);
expect(mimeType!.type, 'image');
expect(mimeType.subtype, 'jpeg');
});

test('should return `image/heic` if ends with `heic`', () {
const fileName = 'dummyFileName.heic';
final mimeType = fileName.mimeType;
final mimeType = fileName.mediaType;
expect(mimeType, isNotNull);
expect(mimeType!.type, 'image');
expect(mimeType.subtype, 'heic');
Expand Down
Loading