[Async Attachment Upload] Initial implementation
Signed-off-by: Sahil Kumar <[email protected]>
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
1589B944F51366A883B3A7A5 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AFDB673F1C9808CE4EC418F /* Pods_Runner.framework */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
@@ -31,7 +32,11 @@
|
||||
/* Begin PBXFileReference section */
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
24CE22BB301621B9BF1A7A7C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
4684439012E1DB1A82103E26 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
4AFDB673F1C9808CE4EC418F /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
59062C6EC2CCFE110AC70AB8 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
@@ -49,12 +54,24 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1589B944F51366A883B3A7A5 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
38CA51382F78F25FC59B6C81 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
24CE22BB301621B9BF1A7A7C /* Pods-Runner.debug.xcconfig */,
|
||||
59062C6EC2CCFE110AC70AB8 /* Pods-Runner.release.xcconfig */,
|
||||
4684439012E1DB1A82103E26 /* Pods-Runner.profile.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -72,6 +89,8 @@
|
||||
9740EEB11CF90186004384FC /* Flutter */,
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
38CA51382F78F25FC59B6C81 /* Pods */,
|
||||
E3E00C71A36D1ABA459667BF /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -98,6 +117,14 @@
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E3E00C71A36D1ABA459667BF /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4AFDB673F1C9808CE4EC418F /* Pods_Runner.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -105,12 +132,14 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
41D6CB24535AC541800DBB07 /* [CP] Check Pods Manifest.lock */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
9DF0031578B883CBE79BDCC8 /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -183,6 +212,28 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||
};
|
||||
41D6CB24535AC541800DBB07 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -197,6 +248,23 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
9DF0031578B883CBE79BDCC8 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
|
||||
+3
@@ -4,4 +4,7 @@
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_persistence/stream_chat_persistence.dart';
|
||||
|
||||
final chatPersistentClient = StreamChatPersistenceClient(
|
||||
logLevel: Level.INFO,
|
||||
connectionMode: ConnectionMode.background,
|
||||
);
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
/// Create a new instance of [StreamChatClient] passing the apikey obtained from your
|
||||
/// project dashboard.
|
||||
final client = StreamChatClient(
|
||||
's2dxdhpxd94g',
|
||||
logLevel: Level.INFO,
|
||||
);
|
||||
)..chatPersistenceClient = chatPersistentClient;
|
||||
|
||||
/// Set the current user and connect the websocket. In a production scenario, this should be done using
|
||||
/// a backend to generate a user token using our server SDK.
|
||||
@@ -20,8 +28,7 @@ void main() async {
|
||||
|
||||
final channel = client.channel('messaging', id: 'godevs');
|
||||
|
||||
// ignore: unawaited_futures
|
||||
channel.watch();
|
||||
await channel.watch();
|
||||
|
||||
runApp(MyApp(client, channel));
|
||||
}
|
||||
@@ -49,6 +56,9 @@ class MyApp extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
theme: ThemeData.light(),
|
||||
darkTheme: ThemeData.dark(),
|
||||
themeMode: ThemeMode.system,
|
||||
builder: (context, widget) {
|
||||
return StreamChat(
|
||||
child: widget,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export 'file_attachment.dart';
|
||||
export 'giphy_attachment.dart';
|
||||
export 'image_attachment.dart';
|
||||
export 'video_attachment.dart';
|
||||
export 'attachment_widget.dart'
|
||||
show AttachmentError, AttachmentSource, AttachmentSourceX;
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
import 'stream_chat_theme.dart';
|
||||
import 'utils.dart';
|
||||
import '../stream_chat_theme.dart';
|
||||
import '../utils.dart';
|
||||
|
||||
class AttachmentTitle extends StatelessWidget {
|
||||
const AttachmentTitle({
|
||||
@@ -0,0 +1,197 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/upload_progress_indicator.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
typedef InProgressBuilder = Widget Function(BuildContext, int, int);
|
||||
typedef FailedBuilder = Widget Function(BuildContext, String);
|
||||
|
||||
class AttachmentUploadStateBuilder extends StatelessWidget {
|
||||
final Message message;
|
||||
final Attachment attachment;
|
||||
final FailedBuilder failedBuilder;
|
||||
final WidgetBuilder successBuilder;
|
||||
final InProgressBuilder inProgressBuilder;
|
||||
|
||||
const AttachmentUploadStateBuilder({
|
||||
Key key,
|
||||
@required this.message,
|
||||
@required this.attachment,
|
||||
this.failedBuilder,
|
||||
this.successBuilder,
|
||||
this.inProgressBuilder,
|
||||
}) : assert(message != null),
|
||||
assert(attachment != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (attachment.uploadState == null) return Offstage();
|
||||
|
||||
final messageId = message.id;
|
||||
final attachmentId = attachment.id;
|
||||
|
||||
var inProgress = inProgressBuilder;
|
||||
inProgress ??= (context, int sent, int total) {
|
||||
return _InProgressState(
|
||||
sent: sent,
|
||||
total: total,
|
||||
attachmentId: attachmentId,
|
||||
);
|
||||
};
|
||||
|
||||
var failed = failedBuilder;
|
||||
failed ??= (context, error) {
|
||||
return _FailedState(
|
||||
error: error,
|
||||
messageId: messageId,
|
||||
attachmentId: attachmentId,
|
||||
);
|
||||
};
|
||||
|
||||
var success = successBuilder;
|
||||
success ??= (context) => _SuccessState();
|
||||
|
||||
return attachment.uploadState.when(
|
||||
inProgress: (sent, total) => inProgress(context, sent, total),
|
||||
success: () => success(context),
|
||||
failed: (error) => failed(context, error),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _IconButton extends StatelessWidget {
|
||||
final Widget icon;
|
||||
final double iconSize;
|
||||
final VoidCallback onPressed;
|
||||
final Color fillColor;
|
||||
|
||||
const _IconButton({
|
||||
Key key,
|
||||
this.icon,
|
||||
this.iconSize = 24.0,
|
||||
this.onPressed,
|
||||
this.fillColor,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: iconSize,
|
||||
width: iconSize,
|
||||
child: RawMaterialButton(
|
||||
elevation: 0,
|
||||
highlightElevation: 0,
|
||||
focusElevation: 0,
|
||||
disabledElevation: 0,
|
||||
hoverElevation: 0,
|
||||
onPressed: onPressed,
|
||||
fillColor:
|
||||
fillColor ?? StreamChatTheme.of(context).colorTheme.overlayDark,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: icon,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InProgressState extends StatelessWidget {
|
||||
final int sent;
|
||||
final int total;
|
||||
final String attachmentId;
|
||||
|
||||
const _InProgressState({
|
||||
Key key,
|
||||
@required this.sent,
|
||||
@required this.total,
|
||||
@required this.attachmentId,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_IconButton(
|
||||
icon: StreamSvgIcon.close(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
),
|
||||
onPressed: () => channel.cancelAttachmentUpload(attachmentId),
|
||||
),
|
||||
Center(
|
||||
child: UploadProgressIndicator(
|
||||
uploaded: sent,
|
||||
total: total,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FailedState extends StatelessWidget {
|
||||
final String error;
|
||||
final String messageId;
|
||||
final String attachmentId;
|
||||
|
||||
const _FailedState({
|
||||
Key key,
|
||||
this.error,
|
||||
@required this.messageId,
|
||||
@required this.attachmentId,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
final theme = StreamChatTheme.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
_IconButton(
|
||||
icon: StreamSvgIcon.retry(
|
||||
color: theme.colorTheme.white,
|
||||
),
|
||||
onPressed: () {
|
||||
return channel.retryAttachmentUpload(messageId, attachmentId);
|
||||
},
|
||||
),
|
||||
Center(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: theme.colorTheme.overlayDark.withOpacity(0.6),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
|
||||
child: Text(
|
||||
'UPLOAD ERROR',
|
||||
style: theme.textTheme.footnote.copyWith(
|
||||
color: theme.colorTheme.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SuccessState extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: CircleAvatar(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.overlayDark,
|
||||
maxRadius: 12.0,
|
||||
child: StreamSvgIcon.check(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
import '../stream_chat_theme.dart';
|
||||
|
||||
enum AttachmentSource {
|
||||
local,
|
||||
network,
|
||||
}
|
||||
|
||||
extension AttachmentSourceX on AttachmentSource {
|
||||
/// The [when] method is the equivalent to pattern matching.
|
||||
/// Its prototype depends on the AttachmentSource defined.
|
||||
T when<T>({
|
||||
@required T Function() local,
|
||||
@required T Function() network,
|
||||
}) {
|
||||
assert(() {
|
||||
if (local == null || network == null) {
|
||||
throw 'check for all possible cases';
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
switch (this) {
|
||||
case AttachmentSource.local:
|
||||
return local();
|
||||
case AttachmentSource.network:
|
||||
return network();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class AttachmentWidget extends StatelessWidget {
|
||||
final Size size;
|
||||
final Message message;
|
||||
final Attachment attachment;
|
||||
final AttachmentSource _source;
|
||||
|
||||
AttachmentSource get source => _source ?? attachment.file != null
|
||||
? AttachmentSource.local
|
||||
: AttachmentSource.network;
|
||||
|
||||
const AttachmentWidget({
|
||||
Key key,
|
||||
@required this.message,
|
||||
@required this.attachment,
|
||||
this.size,
|
||||
AttachmentSource source,
|
||||
}) : _source = source,
|
||||
super(key: key);
|
||||
}
|
||||
|
||||
class AttachmentError extends StatelessWidget {
|
||||
final Size size;
|
||||
|
||||
const AttachmentError({
|
||||
Key key,
|
||||
this.size,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: size?.width,
|
||||
height: size?.height,
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed.withOpacity(.1),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.error_outline,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/utils.dart';
|
||||
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
import '../upload_progress_indicator.dart';
|
||||
import 'attachment_widget.dart';
|
||||
|
||||
class FileAttachment extends AttachmentWidget {
|
||||
final Widget title;
|
||||
final Widget trailing;
|
||||
|
||||
const FileAttachment({
|
||||
Key key,
|
||||
@required Message message,
|
||||
@required Attachment attachment,
|
||||
Size size,
|
||||
this.title,
|
||||
this.trailing,
|
||||
}) : super(key: key, message: message, attachment: attachment, size: size);
|
||||
|
||||
bool get isVideoAttachment => attachment.title?.mimeType?.type == 'video';
|
||||
|
||||
bool get isImageAttachment => attachment.title?.mimeType?.type == 'image';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
child: Container(
|
||||
width: size?.width ?? 100,
|
||||
height: 56.0,
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
child: _getFileTypeImage(context),
|
||||
height: 40.0,
|
||||
width: 33.33,
|
||||
margin: EdgeInsets.all(8.0),
|
||||
),
|
||||
SizedBox(width: 8.0),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
attachment?.title ?? 'File',
|
||||
style: StreamChatTheme.of(context).textTheme.bodyBold,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(height: 3.0),
|
||||
_buildSubtitle(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.0),
|
||||
_buildTrailing(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ShapeBorder _getDefaultShape(BuildContext context) {
|
||||
return RoundedRectangleBorder(
|
||||
side: BorderSide(width: 0.0, color: Colors.transparent),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getFileTypeImage(BuildContext context) {
|
||||
if (isImageAttachment) {
|
||||
return Material(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
type: MaterialType.transparency,
|
||||
shape: _getDefaultShape(context),
|
||||
child: source.when(
|
||||
local: () => Image.memory(
|
||||
attachment.file.bytes,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, obj, trace) {
|
||||
return getFileTypeImage(attachment.extraData['other']);
|
||||
},
|
||||
),
|
||||
network: () => CachedNetworkImage(
|
||||
imageUrl: attachment.imageUrl ??
|
||||
attachment.assetUrl ??
|
||||
attachment.thumbUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (_, obj, trace) {
|
||||
return getFileTypeImage(attachment.extraData['other']);
|
||||
},
|
||||
progressIndicatorBuilder: (context, _, progress) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: const CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isVideoAttachment) {
|
||||
return Material(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
type: MaterialType.transparency,
|
||||
shape: _getDefaultShape(context),
|
||||
child: source.when(
|
||||
local: () => VideoThumbnailImage(
|
||||
video: attachment.file.path,
|
||||
placeholderBuilder: (_) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: const CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
network: () => VideoThumbnailImage(
|
||||
video: attachment.assetUrl,
|
||||
placeholderBuilder: (_) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: const CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return getFileTypeImage(attachment.extraData['mime_type']);
|
||||
}
|
||||
|
||||
Widget _buildButton({
|
||||
Widget icon,
|
||||
double iconSize = 24.0,
|
||||
VoidCallback onPressed,
|
||||
Color fillColor,
|
||||
}) {
|
||||
return Container(
|
||||
height: iconSize,
|
||||
width: iconSize,
|
||||
child: RawMaterialButton(
|
||||
elevation: 0,
|
||||
highlightElevation: 0,
|
||||
focusElevation: 0,
|
||||
disabledElevation: 0,
|
||||
hoverElevation: 0,
|
||||
onPressed: onPressed,
|
||||
fillColor: fillColor,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: icon,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTrailing(BuildContext context) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
final attachmentId = attachment.id;
|
||||
var trailingWidget = trailing;
|
||||
trailingWidget ??= attachment.uploadState?.when(
|
||||
inProgress: (_, __) => Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: _buildButton(
|
||||
icon: StreamSvgIcon.close(color: theme.colorTheme.white),
|
||||
fillColor: theme.colorTheme.overlayDark,
|
||||
onPressed: () => channel.cancelAttachmentUpload(attachmentId),
|
||||
),
|
||||
),
|
||||
success: () => Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: CircleAvatar(
|
||||
backgroundColor: theme.colorTheme.accentBlue,
|
||||
maxRadius: 12.0,
|
||||
child: StreamSvgIcon.check(color: theme.colorTheme.white),
|
||||
),
|
||||
),
|
||||
failed: (_) => Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: _buildButton(
|
||||
icon: StreamSvgIcon.retry(color: theme.colorTheme.white),
|
||||
fillColor: theme.colorTheme.overlayDark,
|
||||
onPressed: () => channel.retryAttachmentUpload(
|
||||
message?.id,
|
||||
attachmentId,
|
||||
),
|
||||
),
|
||||
),
|
||||
) ??
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.cloudDownload(color: theme.colorTheme.black),
|
||||
padding: const EdgeInsets.all(8),
|
||||
visualDensity: VisualDensity.compact,
|
||||
splashRadius: 16,
|
||||
onPressed: () {
|
||||
launchURL(context, attachment.assetUrl);
|
||||
},
|
||||
);
|
||||
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: trailingWidget,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubtitle(BuildContext context) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
final size = attachment.file?.size ?? attachment.extraData['file_size'];
|
||||
final textStyle = theme.textTheme.footnote.copyWith(
|
||||
color: theme.colorTheme.grey,
|
||||
);
|
||||
return attachment.uploadState?.when(
|
||||
inProgress: (sent, total) {
|
||||
return UploadProgressIndicator(
|
||||
uploaded: sent,
|
||||
total: total,
|
||||
showBackground: false,
|
||||
padding: EdgeInsets.zero,
|
||||
textStyle: textStyle,
|
||||
progressIndicatorColor: theme.colorTheme.accentBlue,
|
||||
);
|
||||
},
|
||||
success: () {
|
||||
return Text(
|
||||
'${fileSize(size, 1)}/${fileSize(size, 1)}',
|
||||
style: textStyle,
|
||||
);
|
||||
},
|
||||
failed: (_) => Text('UPLOAD ERROR', style: textStyle),
|
||||
) ??
|
||||
Text('${fileSize(size)}', style: textStyle);
|
||||
}
|
||||
}
|
||||
+52
-75
@@ -1,48 +1,42 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'attachment_error.dart';
|
||||
import 'full_screen_media.dart';
|
||||
import '../full_screen_media.dart';
|
||||
import '../stream_chat_theme.dart';
|
||||
import '../stream_svg_icon.dart';
|
||||
import 'attachment_widget.dart';
|
||||
|
||||
class GiphyAttachment extends StatelessWidget {
|
||||
final Attachment attachment;
|
||||
class GiphyAttachment extends AttachmentWidget {
|
||||
final MessageTheme messageTheme;
|
||||
final Message message;
|
||||
final Size size;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
final ValueChanged<ReturnActionType> onReturnAction;
|
||||
|
||||
const GiphyAttachment({
|
||||
Key key,
|
||||
this.attachment,
|
||||
@required Message message,
|
||||
@required Attachment attachment,
|
||||
Size size,
|
||||
this.messageTheme,
|
||||
this.message,
|
||||
this.size,
|
||||
this.onShowMessage,
|
||||
this.onReturnAction,
|
||||
}) : super(key: key);
|
||||
}) : super(key: key, message: message, attachment: attachment, size: size);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (attachment.thumbUrl == null &&
|
||||
attachment.imageUrl == null &&
|
||||
attachment.assetUrl == null) {
|
||||
return AttachmentError(
|
||||
attachment: attachment,
|
||||
);
|
||||
final imageUrl =
|
||||
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl;
|
||||
if (imageUrl == null && source == AttachmentSource.network) {
|
||||
return AttachmentError();
|
||||
}
|
||||
|
||||
return attachment.actions != null
|
||||
? _buildSendingAttachment(context)
|
||||
: _buildSentAttachment(context);
|
||||
if (attachment.actions != null) {
|
||||
return _buildSendingAttachment(context, imageUrl);
|
||||
}
|
||||
return _buildSentAttachment(context, imageUrl);
|
||||
}
|
||||
|
||||
Widget _buildSendingAttachment(context) {
|
||||
Widget _buildSendingAttachment(BuildContext context, String imageUrl) {
|
||||
final streamChannel = StreamChannel.of(context);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -67,9 +61,7 @@ class GiphyAttachment extends StatelessWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
_onImageTap(context);
|
||||
},
|
||||
onTap: () => _onImageTap(context),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(8),
|
||||
@@ -87,13 +79,10 @@ class GiphyAttachment extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
imageUrl: attachment.thumbUrl ??
|
||||
attachment.imageUrl ??
|
||||
attachment.assetUrl,
|
||||
errorWidget: (context, url, error) => AttachmentError(
|
||||
attachment: attachment,
|
||||
size: size,
|
||||
),
|
||||
imageUrl: imageUrl,
|
||||
errorWidget: (context, url, error) {
|
||||
return AttachmentError(size: size);
|
||||
},
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
@@ -305,44 +294,38 @@ class GiphyAttachment extends StatelessWidget {
|
||||
}
|
||||
|
||||
void _onImageTap(BuildContext context) async {
|
||||
var res = await Navigator.push(context, MaterialPageRoute(
|
||||
builder: (_) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
|
||||
return StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments: [
|
||||
attachment,
|
||||
],
|
||||
userName: message.user.name,
|
||||
sentAt: message.createdAt,
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
),
|
||||
);
|
||||
},
|
||||
));
|
||||
|
||||
if (res != null) {
|
||||
onReturnAction(res);
|
||||
}
|
||||
final res = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
return StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments: [attachment],
|
||||
userName: message.user.name,
|
||||
sentAt: message.createdAt,
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
if (res != null) onReturnAction(res);
|
||||
}
|
||||
|
||||
Widget _buildSentAttachment(context) {
|
||||
Widget _buildSentAttachment(BuildContext context, String imageUrl) {
|
||||
return Container(
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
var res =
|
||||
final res =
|
||||
await Navigator.push(context, MaterialPageRoute(builder: (_) {
|
||||
var channel = StreamChannel.of(context).channel;
|
||||
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
return StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments: [
|
||||
attachment,
|
||||
],
|
||||
mediaAttachments: [attachment],
|
||||
userName: message.user.name,
|
||||
sentAt: message.createdAt,
|
||||
message: message,
|
||||
@@ -350,10 +333,7 @@ class GiphyAttachment extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}));
|
||||
|
||||
if (res != null) {
|
||||
onReturnAction(res);
|
||||
}
|
||||
if (res != null) onReturnAction(res);
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
@@ -369,13 +349,10 @@ class GiphyAttachment extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
imageUrl: attachment.thumbUrl ??
|
||||
attachment.imageUrl ??
|
||||
attachment.assetUrl,
|
||||
errorWidget: (context, url, error) => AttachmentError(
|
||||
attachment: attachment,
|
||||
size: size,
|
||||
),
|
||||
imageUrl: imageUrl,
|
||||
errorWidget: (context, url, error) {
|
||||
return AttachmentError(size: size);
|
||||
},
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
Positioned(
|
||||
@@ -0,0 +1,139 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/attachment/attachment_upload_state_builder.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
import 'attachment_title.dart';
|
||||
import '../full_screen_media.dart';
|
||||
import '../stream_chat_theme.dart';
|
||||
import 'attachment_widget.dart';
|
||||
|
||||
class ImageAttachment extends AttachmentWidget {
|
||||
final MessageTheme messageTheme;
|
||||
final bool showTitle;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
final ValueChanged<ReturnActionType> onReturnAction;
|
||||
final VoidCallback onAttachmentTap;
|
||||
|
||||
const ImageAttachment({
|
||||
Key key,
|
||||
@required Message message,
|
||||
@required Attachment attachment,
|
||||
Size size,
|
||||
this.messageTheme,
|
||||
this.showTitle = false,
|
||||
this.onShowMessage,
|
||||
this.onReturnAction,
|
||||
this.onAttachmentTap,
|
||||
}) : super(key: key, message: message, attachment: attachment, size: size);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return source.when(
|
||||
local: () {
|
||||
if (attachment.localUri == null) {
|
||||
return AttachmentError(size: size);
|
||||
}
|
||||
return _buildImageAttachment(
|
||||
context,
|
||||
Image.memory(
|
||||
attachment.file.bytes,
|
||||
height: size?.height,
|
||||
width: size?.width,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, _, __) {
|
||||
return Image.asset(
|
||||
'images/placeholder.png',
|
||||
package: 'stream_chat_flutter',
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
network: () {
|
||||
final imageUrl =
|
||||
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl;
|
||||
if (imageUrl == null) {
|
||||
return AttachmentError(size: size);
|
||||
}
|
||||
return _buildImageAttachment(
|
||||
context,
|
||||
CachedNetworkImage(
|
||||
height: size?.height,
|
||||
width: size?.width,
|
||||
placeholder: (_, __) {
|
||||
return Container(
|
||||
width: size?.width,
|
||||
height: size?.height,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
},
|
||||
imageUrl: imageUrl,
|
||||
errorWidget: (context, url, error) {
|
||||
return AttachmentError(size: size);
|
||||
},
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImageAttachment(BuildContext context, Widget imageWidget) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints.loose(size),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: onAttachmentTap ??
|
||||
() async {
|
||||
final result = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
return StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments: [attachment],
|
||||
userName: message.user.name,
|
||||
sentAt: message.createdAt,
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
if (result != null) onReturnAction(result);
|
||||
},
|
||||
child: imageWidget,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: AttachmentUploadStateBuilder(
|
||||
message: message,
|
||||
attachment: attachment,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (showTitle && attachment.title != null)
|
||||
Material(
|
||||
color: messageTheme.messageBackgroundColor,
|
||||
child: AttachmentTitle(
|
||||
messageTheme: messageTheme,
|
||||
attachment: attachment,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/full_screen_media.dart';
|
||||
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
import 'attachment_title.dart';
|
||||
import 'attachment_upload_state_builder.dart';
|
||||
import 'attachment_widget.dart';
|
||||
|
||||
class VideoAttachment extends AttachmentWidget {
|
||||
final MessageTheme messageTheme;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
final ValueChanged<ReturnActionType> onReturnAction;
|
||||
|
||||
const VideoAttachment({
|
||||
Key key,
|
||||
@required Message message,
|
||||
@required Attachment attachment,
|
||||
Size size,
|
||||
this.messageTheme,
|
||||
this.onShowMessage,
|
||||
this.onReturnAction,
|
||||
}) : super(key: key, message: message, attachment: attachment, size: size);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return source.when(
|
||||
local: () {
|
||||
if (attachment.file == null) {
|
||||
return AttachmentError(size: size);
|
||||
}
|
||||
return _buildVideoAttachment(
|
||||
context,
|
||||
VideoThumbnailImage(
|
||||
video: attachment.file.path,
|
||||
height: size?.height,
|
||||
width: size?.width,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __) => AttachmentError(size: size),
|
||||
),
|
||||
);
|
||||
},
|
||||
network: () {
|
||||
if (attachment.assetUrl == null) {
|
||||
return AttachmentError(size: size);
|
||||
}
|
||||
return _buildVideoAttachment(
|
||||
context,
|
||||
VideoThumbnailImage(
|
||||
video: attachment.assetUrl,
|
||||
height: size?.height,
|
||||
width: size?.width,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __) => AttachmentError(size: size),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVideoAttachment(BuildContext context, Widget videoWidget) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints.loose(size),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
final res = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments: [attachment],
|
||||
userName: message.user.name,
|
||||
sentAt: message.createdAt,
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (res != null) onReturnAction(res);
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: size?.height,
|
||||
width: size?.width,
|
||||
child: videoWidget,
|
||||
),
|
||||
Center(
|
||||
child: Material(
|
||||
shape: CircleBorder(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Icon(Icons.play_arrow),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: AttachmentUploadStateBuilder(
|
||||
message: message,
|
||||
attachment: attachment,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (attachment.title != null)
|
||||
Material(
|
||||
color: messageTheme.messageBackgroundColor,
|
||||
child: AttachmentTitle(
|
||||
messageTheme: messageTheme,
|
||||
attachment: attachment,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import '../stream_chat_flutter.dart';
|
||||
|
||||
class AttachmentError extends StatelessWidget {
|
||||
final Attachment attachment;
|
||||
final Size size;
|
||||
|
||||
const AttachmentError({
|
||||
Key key,
|
||||
@required this.attachment,
|
||||
this.size,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (attachment.localUri != null) {
|
||||
return Image.file(
|
||||
File(attachment.localUri.path),
|
||||
);
|
||||
}
|
||||
return Center(
|
||||
child: Container(
|
||||
width: size?.width,
|
||||
height: size?.height ?? 200,
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed.withOpacity(.1),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.error_outline,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'extension.dart';
|
||||
|
||||
abstract class AttachmentUploader {
|
||||
Future<String> uploadImage(
|
||||
PlatformFile file, {
|
||||
ProgressCallback onSendProgress,
|
||||
});
|
||||
|
||||
Future<String> uploadFile(
|
||||
PlatformFile file, {
|
||||
ProgressCallback onSendProgress,
|
||||
});
|
||||
}
|
||||
|
||||
class StreamAttachmentUploader implements AttachmentUploader {
|
||||
final Channel _channel;
|
||||
|
||||
const StreamAttachmentUploader(this._channel);
|
||||
|
||||
@override
|
||||
Future<String> uploadImage(
|
||||
PlatformFile file, {
|
||||
ProgressCallback onSendProgress,
|
||||
}) async {
|
||||
final filename = file.path?.split('/')?.last;
|
||||
final mimeType = filename.mimeType;
|
||||
final res = await _channel.sendImage(
|
||||
await MultipartFile.fromFile(
|
||||
file.path,
|
||||
filename: filename,
|
||||
contentType: mimeType,
|
||||
),
|
||||
onSendProgress: onSendProgress,
|
||||
);
|
||||
return res.file;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> uploadFile(
|
||||
PlatformFile file, {
|
||||
ProgressCallback onSendProgress,
|
||||
}) async {
|
||||
final filename = file.path?.split('/')?.last;
|
||||
final mimeType = filename.mimeType;
|
||||
final res = await _channel.sendFile(
|
||||
await MultipartFile.fromFile(
|
||||
file.path,
|
||||
filename: filename,
|
||||
contentType: mimeType,
|
||||
),
|
||||
onSendProgress: onSendProgress,
|
||||
);
|
||||
return res.file;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
import 'attachment/attachment.dart';
|
||||
|
||||
class ChannelFileDisplayScreen extends StatefulWidget {
|
||||
/// The sorting used for the channels matching the filters.
|
||||
/// Sorting is based on field and direction, multiple sorting options can be provided.
|
||||
@@ -164,6 +166,7 @@ class _ChannelFileDisplayScreenState extends State<ChannelFileDisplayScreen> {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: FileAttachment(
|
||||
message: media.values.toList()[position],
|
||||
attachment: media.keys.toList()[position],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import 'attachment/attachment.dart';
|
||||
|
||||
class ChannelMediaDisplayScreen extends StatefulWidget {
|
||||
/// The sorting used for the channels matching the filters.
|
||||
/// Sorting is based on field and direction, multiple sorting options can be provided.
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
|
||||
class ICompressVideoService {
|
||||
static final ICompressVideoService instance = ICompressVideoService._();
|
||||
final _lock = Lock();
|
||||
|
||||
ICompressVideoService._();
|
||||
|
||||
Future<MediaInfo> compress(String path) async {
|
||||
return _lock.synchronized(() {
|
||||
return VideoCompress.compressVideo(
|
||||
path,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ICompressVideoService get compressVideoService =>
|
||||
ICompressVideoService.instance;
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:characters/characters.dart';
|
||||
import 'package:emojis/emoji.dart';
|
||||
import 'package:http_parser/http_parser.dart' as http_parser;
|
||||
import 'package:mime/mime.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
final _emojis = Emoji.all();
|
||||
|
||||
@@ -23,16 +23,6 @@ extension StringExtension on String {
|
||||
if (characters.length > 3) return false;
|
||||
return characters.every((c) => _emojis.map((e) => e.char).contains(c));
|
||||
}
|
||||
|
||||
/// Returns the mime type from the passed file name.
|
||||
http_parser.MediaType get mimeType {
|
||||
if (this == null) return null;
|
||||
if (toLowerCase().endsWith('heic')) {
|
||||
return http_parser.MediaType.parse('image/heic');
|
||||
} else {
|
||||
return http_parser.MediaType.parse(lookupMimeType(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// List extension
|
||||
@@ -43,3 +33,14 @@ extension IterableX<T> on Iterable<T> {
|
||||
yield e;
|
||||
}).skip(1).toList(growable: false);
|
||||
}
|
||||
|
||||
///
|
||||
extension PlatformFileX on PlatformFile {
|
||||
///
|
||||
AttachmentFile get toAttachmentFile => AttachmentFile(
|
||||
path: path,
|
||||
name: name,
|
||||
bytes: bytes,
|
||||
size: size,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,207 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/utils.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import 'media_utils.dart';
|
||||
|
||||
enum FileAttachmentType { local, online }
|
||||
|
||||
class FileAttachment extends StatefulWidget {
|
||||
final Attachment attachment;
|
||||
final Size size;
|
||||
final Widget trailing;
|
||||
final FileAttachmentType attachmentType;
|
||||
final PlatformFile file;
|
||||
|
||||
const FileAttachment({
|
||||
Key key,
|
||||
@required this.attachment,
|
||||
this.size,
|
||||
this.trailing,
|
||||
this.attachmentType = FileAttachmentType.online,
|
||||
this.file,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_FileAttachmentState createState() => _FileAttachmentState();
|
||||
}
|
||||
|
||||
class _FileAttachmentState extends State<FileAttachment> {
|
||||
VideoPlayerController _controller;
|
||||
Future<void> _initializeVideoPlayerFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (MediaUtils.getMimeType(widget.attachment.title)?.type == 'video') {
|
||||
if (widget.attachmentType == FileAttachmentType.online) {
|
||||
_controller = VideoPlayerController.network(
|
||||
widget.attachment.assetUrl,
|
||||
);
|
||||
} else {
|
||||
_controller = VideoPlayerController.file(
|
||||
File.fromRawPath(widget.file.bytes),
|
||||
);
|
||||
}
|
||||
|
||||
_initializeVideoPlayerFuture = _controller.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
child: Container(
|
||||
width: widget.size?.width ?? 100,
|
||||
height: 56.0,
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
child: _getFileTypeImage(),
|
||||
height: 40.0,
|
||||
width: 33.33,
|
||||
margin: EdgeInsets.all(8.0),
|
||||
),
|
||||
SizedBox(width: 8.0),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.attachment?.title ?? 'File',
|
||||
style: StreamChatTheme.of(context).textTheme.bodyBold,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
SizedBox(height: 3.0),
|
||||
Text(
|
||||
'${filesize(widget.attachment.extraData['file_size'])}',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.footnote
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.0),
|
||||
Material(
|
||||
type: MaterialType.transparency,
|
||||
child: widget.trailing ??
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.cloudDownload(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
visualDensity: VisualDensity.compact,
|
||||
splashRadius: 16,
|
||||
onPressed: () {
|
||||
launchURL(context, widget.attachment.assetUrl);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getFileTypeImage() {
|
||||
if ((MediaUtils.getMimeType(widget.attachment.title)?.type == 'image')) {
|
||||
switch (widget.attachmentType) {
|
||||
case FileAttachmentType.local:
|
||||
return Image.memory(
|
||||
widget.file.bytes,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, obj, trace) {
|
||||
return getFileTypeImage(widget.attachment.extraData['other']);
|
||||
},
|
||||
);
|
||||
break;
|
||||
case FileAttachmentType.online:
|
||||
return CachedNetworkImage(
|
||||
imageUrl: widget.attachment.imageUrl ??
|
||||
widget.attachment.assetUrl ??
|
||||
widget.attachment.thumbUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (_, obj, trace) {
|
||||
return getFileTypeImage(widget.attachment.extraData['other']);
|
||||
},
|
||||
progressIndicatorBuilder: (context, _, progress) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: CircularProgressIndicator(
|
||||
backgroundColor:
|
||||
StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ((MediaUtils.getMimeType(widget.attachment.title)?.type == 'video')) {
|
||||
switch (widget.attachmentType) {
|
||||
case FileAttachmentType.local:
|
||||
return FutureBuilder<File>(
|
||||
future: VideoCompress.getFileThumbnail(widget.file.path),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Image.asset(
|
||||
'images/placeholder.png',
|
||||
package: 'stream_chat_flutter',
|
||||
);
|
||||
}
|
||||
|
||||
return Image.file(
|
||||
snapshot.data,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
},
|
||||
);
|
||||
break;
|
||||
case FileAttachmentType.online:
|
||||
return FutureBuilder(
|
||||
future: _initializeVideoPlayerFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return AspectRatio(
|
||||
aspectRatio: _controller.value.aspectRatio,
|
||||
child: VideoPlayer(_controller),
|
||||
);
|
||||
} else {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
},
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return getFileTypeImage(widget.attachment.extraData['mime_type']);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -48,36 +51,34 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
|
||||
int _currentPage;
|
||||
|
||||
List<VideoPackage> videoPackages = [];
|
||||
final videoPackages = <String, VideoPackage>{};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller =
|
||||
AnimationController(vsync: this, duration: Duration(milliseconds: 300));
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: Duration(milliseconds: 300),
|
||||
);
|
||||
_pageController = PageController(initialPage: widget.startIndex);
|
||||
_currentPage = widget.startIndex;
|
||||
widget.mediaAttachments
|
||||
.where((element) => element.type == 'video')
|
||||
.toList()
|
||||
.forEach((element) {
|
||||
videoPackages.add(VideoPackage(
|
||||
context,
|
||||
element,
|
||||
() {
|
||||
setState(() {});
|
||||
},
|
||||
showControls: true,
|
||||
));
|
||||
});
|
||||
for (final attachment in widget.mediaAttachments) {
|
||||
if (attachment.type != 'video') continue;
|
||||
final package = VideoPackage(attachment, showControls: true);
|
||||
videoPackages[attachment.id] = package;
|
||||
}
|
||||
initializePlayers();
|
||||
}
|
||||
|
||||
Future<void> initializePlayers() async {
|
||||
await Future.wait(videoPackages.values.map(
|
||||
(it) => it.initialize(),
|
||||
));
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var videoAttachments = widget.mediaAttachments
|
||||
.where((element) => element.type == 'video')
|
||||
.toList();
|
||||
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: Stack(
|
||||
@@ -92,14 +93,18 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
_currentPage = val;
|
||||
});
|
||||
},
|
||||
itemBuilder: (context, position) {
|
||||
if (widget.mediaAttachments[position].type == 'image' ||
|
||||
widget.mediaAttachments[position].type == 'giphy') {
|
||||
itemBuilder: (context, index) {
|
||||
final attachment = widget.mediaAttachments[index];
|
||||
if (attachment.type == 'image' ||
|
||||
attachment.type == 'giphy') {
|
||||
final imageUrl = attachment.imageUrl ??
|
||||
attachment.assetUrl ??
|
||||
attachment.thumbUrl;
|
||||
return PhotoView(
|
||||
imageProvider: CachedNetworkImageProvider(
|
||||
widget.mediaAttachments[position].imageUrl ??
|
||||
widget.mediaAttachments[position].assetUrl ??
|
||||
widget.mediaAttachments[position].thumbUrl),
|
||||
imageProvider:
|
||||
imageUrl == null && attachment.localUri != null
|
||||
? Image.memory(attachment.file.bytes).image
|
||||
: CachedNetworkImageProvider(imageUrl),
|
||||
maxScale: PhotoViewComputedScale.covered,
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
heroAttributes: PhotoViewHeroAttributes(
|
||||
@@ -125,12 +130,9 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
}
|
||||
},
|
||||
);
|
||||
} else if (widget.mediaAttachments[position].type ==
|
||||
'video') {
|
||||
var controllerPackage = videoPackages[videoAttachments
|
||||
.indexOf(widget.mediaAttachments[position])];
|
||||
|
||||
if (!controllerPackage.initialised) {
|
||||
} else if (attachment.type == 'video') {
|
||||
final controller = videoPackages[attachment.id];
|
||||
if (!controller.initialized) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
@@ -151,7 +153,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
vertical: 50.0,
|
||||
),
|
||||
child: Chewie(
|
||||
controller: controllerPackage.chewieController,
|
||||
controller: controller.chewieController,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -188,7 +190,6 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
totalPages: widget.mediaAttachments.length,
|
||||
mediaAttachments: widget.mediaAttachments,
|
||||
message: widget.message,
|
||||
videoPackages: videoPackages,
|
||||
mediaSelectedCallBack: (val) {
|
||||
setState(() {
|
||||
_currentPage = val;
|
||||
@@ -224,54 +225,58 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
videoPackages.forEach((element) {
|
||||
element.dispose();
|
||||
});
|
||||
void dispose() async {
|
||||
for (final package in videoPackages.values) {
|
||||
await package.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class VideoPackage {
|
||||
VideoPlayerController _videoPlayerController;
|
||||
final bool _showControls;
|
||||
final bool _autoInitialize;
|
||||
final VideoPlayerController _videoPlayerController;
|
||||
ChewieController _chewieController;
|
||||
bool initialised = false;
|
||||
VoidCallback onInit;
|
||||
BuildContext context;
|
||||
bool showControls;
|
||||
|
||||
///
|
||||
VideoPackage(this.context, Attachment attachment, this.onInit,
|
||||
{this.showControls = false}) {
|
||||
_videoPlayerController = VideoPlayerController.network(attachment.assetUrl);
|
||||
_videoPlayerController.initialize().whenComplete(() {
|
||||
initialised = true;
|
||||
_chewieController = ChewieController(
|
||||
videoPlayerController: _videoPlayerController,
|
||||
autoInitialize: true,
|
||||
showControls: showControls,
|
||||
aspectRatio: _videoPlayerController.value.aspectRatio,
|
||||
);
|
||||
onInit();
|
||||
});
|
||||
|
||||
VoidCallback errorListener;
|
||||
errorListener = () {
|
||||
if (_videoPlayerController.value.hasError) {
|
||||
Navigator.pop(context);
|
||||
launchURL(context, attachment.titleLink);
|
||||
}
|
||||
_videoPlayerController.removeListener(errorListener);
|
||||
};
|
||||
_videoPlayerController.addListener(errorListener);
|
||||
}
|
||||
|
||||
VideoPlayerController get videoPlayer => _videoPlayerController;
|
||||
|
||||
ChewieController get chewieController => _chewieController;
|
||||
|
||||
void dispose() {
|
||||
_videoPlayerController.dispose();
|
||||
_chewieController.dispose();
|
||||
bool get initialized => _videoPlayerController.value.initialized;
|
||||
|
||||
VideoPackage(
|
||||
Attachment attachment, {
|
||||
bool showControls = false,
|
||||
bool autoInitialize = true,
|
||||
}) : assert(attachment != null),
|
||||
_showControls = showControls,
|
||||
_autoInitialize = autoInitialize,
|
||||
_videoPlayerController = attachment.localUri != null
|
||||
? VideoPlayerController.file(File.fromUri(attachment.localUri))
|
||||
: VideoPlayerController.network(attachment.assetUrl);
|
||||
|
||||
Future<void> initialize() {
|
||||
return _videoPlayerController.initialize().then((_) {
|
||||
_chewieController = ChewieController(
|
||||
videoPlayerController: _videoPlayerController,
|
||||
autoInitialize: _autoInitialize,
|
||||
showControls: _showControls,
|
||||
aspectRatio: _videoPlayerController.value.aspectRatio,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void addListener(VoidCallback listener) {
|
||||
return _videoPlayerController.addListener(listener);
|
||||
}
|
||||
|
||||
void removeListener(VoidCallback listener) {
|
||||
return _videoPlayerController.removeListener(listener);
|
||||
}
|
||||
|
||||
Future<void> dispose() {
|
||||
_chewieController?.dispose();
|
||||
return _videoPlayerController?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,10 +105,9 @@ class ImageActionsModal extends StatelessWidget {
|
||||
() {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
StreamChat.of(context).client.deleteMessage(
|
||||
message,
|
||||
StreamChannel.of(context).channel.cid,
|
||||
);
|
||||
StreamChannel.of(context)
|
||||
.channel
|
||||
.deleteMessage(message);
|
||||
},
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
),
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'attachment_error.dart';
|
||||
import 'attachment_title.dart';
|
||||
import 'full_screen_media.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
class ImageAttachment extends StatelessWidget {
|
||||
final Attachment attachment;
|
||||
final Message message;
|
||||
final MessageTheme messageTheme;
|
||||
final Size size;
|
||||
final bool showTitle;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
final ValueChanged<ReturnActionType> onReturnAction;
|
||||
|
||||
const ImageAttachment({
|
||||
Key key,
|
||||
@required this.attachment,
|
||||
@required this.message,
|
||||
@required this.size,
|
||||
this.messageTheme,
|
||||
this.showTitle = true,
|
||||
this.onShowMessage,
|
||||
this.onReturnAction,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (attachment.thumbUrl == null &&
|
||||
attachment.imageUrl == null &&
|
||||
attachment.assetUrl == null) {
|
||||
return AttachmentError(
|
||||
attachment: attachment,
|
||||
);
|
||||
}
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints.loose(size),
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
var result = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
|
||||
return StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments: [
|
||||
attachment,
|
||||
],
|
||||
userName: message.user.name,
|
||||
sentAt: message.createdAt,
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
onReturnAction(result);
|
||||
}
|
||||
},
|
||||
child: CachedNetworkImage(
|
||||
height: size?.height,
|
||||
width: size?.width,
|
||||
placeholder: (_, __) {
|
||||
return Container(
|
||||
width: size?.width,
|
||||
height: size?.height,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
},
|
||||
imageUrl: attachment.thumbUrl ??
|
||||
attachment.imageUrl ??
|
||||
attachment.assetUrl,
|
||||
errorWidget: (context, url, error) => AttachmentError(
|
||||
attachment: attachment,
|
||||
size: size,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showTitle && attachment.title != null)
|
||||
Material(
|
||||
color: messageTheme.messageBackgroundColor,
|
||||
child: AttachmentTitle(
|
||||
messageTheme: messageTheme,
|
||||
attachment: attachment,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (showTitle &&
|
||||
(attachment.titleLink != null || attachment.ogScrapeUrl != null))
|
||||
Positioned.fill(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => launchURL(
|
||||
context,
|
||||
attachment.titleLink ?? attachment.ogScrapeUrl,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,12 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:esys_flutter_share/esys_flutter_share.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
@@ -28,7 +28,6 @@ class ImageFooter extends StatefulWidget implements PreferredSizeWidget {
|
||||
final List<Attachment> mediaAttachments;
|
||||
final Message message;
|
||||
|
||||
final List<VideoPackage> videoPackages;
|
||||
final ValueChanged<int> mediaSelectedCallBack;
|
||||
|
||||
/// Creates a channel header
|
||||
@@ -41,7 +40,6 @@ class ImageFooter extends StatefulWidget implements PreferredSizeWidget {
|
||||
this.totalPages = 0,
|
||||
this.mediaAttachments,
|
||||
this.message,
|
||||
this.videoPackages,
|
||||
this.mediaSelectedCallBack,
|
||||
}) : preferredSize = Size.fromHeight(kToolbarHeight),
|
||||
super(key: key);
|
||||
@@ -214,17 +212,14 @@ class _ImageFooterState extends State<ImageFooter> {
|
||||
itemBuilder: (context, index) {
|
||||
Widget media;
|
||||
final attachment = widget.mediaAttachments[index];
|
||||
|
||||
if (attachment.type == 'video') {
|
||||
var controllerPackage = widget.videoPackages[
|
||||
videoAttachments.indexOf(attachment)];
|
||||
|
||||
media = InkWell(
|
||||
onTap: () => widget.mediaSelectedCallBack(index),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.cover,
|
||||
child: Chewie(
|
||||
controller: controllerPackage.chewieController,
|
||||
child: VideoThumbnailImage(
|
||||
video: attachment.file?.path ??
|
||||
attachment.assetUrl,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'package:stream_chat_flutter/src/full_screen_media.dart';
|
||||
@@ -9,12 +8,14 @@ class ImageGroup extends StatelessWidget {
|
||||
Key key,
|
||||
@required this.images,
|
||||
@required this.message,
|
||||
@required this.messageTheme,
|
||||
@required this.size,
|
||||
this.onShowMessage,
|
||||
}) : super(key: key);
|
||||
|
||||
final List<Attachment> images;
|
||||
final Message message;
|
||||
final MessageTheme messageTheme;
|
||||
final Size size;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
|
||||
@@ -129,14 +130,12 @@ class ImageGroup extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildImage(BuildContext context, int index) {
|
||||
return GestureDetector(
|
||||
onTap: () => _onTap(context, index),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: images[index].imageUrl ??
|
||||
images[index].thumbUrl ??
|
||||
images[index].assetUrl,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
return ImageAttachment(
|
||||
attachment: images[index],
|
||||
size: size,
|
||||
message: message,
|
||||
messageTheme: messageTheme,
|
||||
onAttachmentTap: () => _onTap(context, index),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import 'package:http_parser/http_parser.dart' as http_parser;
|
||||
import 'package:mime/mime.dart';
|
||||
|
||||
class MediaUtils {
|
||||
static http_parser.MediaType getMimeType(String filename) {
|
||||
http_parser.MediaType mimeType;
|
||||
if (filename != null) {
|
||||
if (filename.toLowerCase().endsWith('heic')) {
|
||||
mimeType = http_parser.MediaType.parse('image/heic');
|
||||
} else {
|
||||
mimeType = http_parser.MediaType.parse(lookupMimeType(filename));
|
||||
}
|
||||
}
|
||||
|
||||
return mimeType;
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,6 @@ class MessageActionsModal extends StatefulWidget {
|
||||
final ShapeBorder messageShape;
|
||||
final ShapeBorder attachmentShape;
|
||||
final DisplayWidget showUserAvatar;
|
||||
final Map<String, VideoPackage> videoPackages;
|
||||
|
||||
const MessageActionsModal({
|
||||
Key key,
|
||||
@@ -54,7 +53,6 @@ class MessageActionsModal extends StatefulWidget {
|
||||
this.messageShape,
|
||||
this.attachmentShape,
|
||||
this.reverse = false,
|
||||
this.videoPackages,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -182,7 +180,6 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
showSendingIndicator: false,
|
||||
shape: widget.messageShape,
|
||||
attachmentShape: widget.attachmentShape,
|
||||
videoPackages: widget.videoPackages,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
@@ -298,10 +295,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
if (answer) {
|
||||
try {
|
||||
Navigator.pop(context);
|
||||
await StreamChat.of(context).client.deleteMessage(
|
||||
widget.message,
|
||||
StreamChannel.of(context).channel.cid,
|
||||
);
|
||||
await StreamChannel.of(context).channel.deleteMessage(widget.message);
|
||||
} catch (err) {
|
||||
_showErrorAlert();
|
||||
}
|
||||
@@ -570,10 +564,9 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
final client = StreamChat.of(context).client;
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
if (isUpdateFailed) {
|
||||
client.updateMessage(widget.message, channel.cid);
|
||||
channel.updateMessage(widget.message);
|
||||
} else {
|
||||
channel.sendMessage(widget.message);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:emojis/emoji.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
@@ -11,7 +11,7 @@ import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:stream_chat_flutter/src/compress_video_service.dart';
|
||||
import 'package:stream_chat_flutter/src/video_service.dart';
|
||||
import 'package:stream_chat_flutter/src/media_list_view.dart';
|
||||
import 'package:stream_chat_flutter/src/message_list_view.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
@@ -19,16 +19,16 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/user_avatar.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'package:substring_highlight/substring_highlight.dart';
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'attachment_uploader.dart';
|
||||
import 'attachment/attachment.dart';
|
||||
import 'extension.dart';
|
||||
import 'quoted_message_widget.dart';
|
||||
import 'video_thumbnail_image.dart';
|
||||
|
||||
typedef AttachmentThumbnailBuilder = Widget Function(
|
||||
BuildContext,
|
||||
_SendingAttachment,
|
||||
Attachment,
|
||||
);
|
||||
|
||||
enum ActionsLocation {
|
||||
@@ -44,7 +44,7 @@ enum DefaultAttachmentTypes {
|
||||
|
||||
const _kMinMediaPickerSize = 360.0;
|
||||
|
||||
const _kMaxAttachmentSize = 20480; //20MB
|
||||
const _kMaxAttachmentSize = 20971520; // 20MB in Bytes
|
||||
|
||||
/// Inactive state
|
||||
/// 
|
||||
@@ -99,7 +99,6 @@ class MessageInput extends StatefulWidget {
|
||||
this.maxHeight = 150,
|
||||
this.keyboardType = TextInputType.multiline,
|
||||
this.disableAttachments = false,
|
||||
this.attachmentUploader,
|
||||
this.initialMessage,
|
||||
this.textEditingController,
|
||||
this.actions,
|
||||
@@ -135,9 +134,6 @@ class MessageInput extends StatefulWidget {
|
||||
/// If true the attachments button will not be displayed
|
||||
final bool disableAttachments;
|
||||
|
||||
/// A delegate to upload attachments
|
||||
final AttachmentUploader attachmentUploader;
|
||||
|
||||
/// The text controller of the TextField
|
||||
final TextEditingController textEditingController;
|
||||
|
||||
@@ -178,7 +174,7 @@ class MessageInput extends StatefulWidget {
|
||||
}
|
||||
|
||||
class MessageInputState extends State<MessageInput> {
|
||||
final _attachments = <String, _SendingAttachment>{};
|
||||
final _attachments = <String, Attachment>{};
|
||||
final List<User> _mentionedUsers = [];
|
||||
|
||||
final _imagePicker = ImagePicker();
|
||||
@@ -204,8 +200,6 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
bool get _hasQuotedMessage => widget.quotedMessage != null;
|
||||
|
||||
AttachmentUploader _attachmentUploader;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -391,8 +385,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: AnimatedCrossFade(
|
||||
crossFadeState: ((_messageIsPresent || _attachments.isNotEmpty) &&
|
||||
_attachments.values.every((a) => a.isUploaded == true))
|
||||
crossFadeState: (_messageIsPresent || _attachments.isNotEmpty)
|
||||
? CrossFadeState.showFirst
|
||||
: CrossFadeState.showSecond,
|
||||
firstChild: _buildSendButton(context),
|
||||
@@ -786,7 +779,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
Widget _buildFilePickerSection() {
|
||||
final _attachmentContainsFile = _attachments.values.any((it) {
|
||||
return it.attachmentType == 'file';
|
||||
return it.type == 'file';
|
||||
});
|
||||
|
||||
Color _getIconColor(int index) {
|
||||
@@ -943,7 +936,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
Widget _buildPickerSection() {
|
||||
final _attachmentContainsFile = _attachments.values.any((it) {
|
||||
return it.attachmentType == 'file';
|
||||
return it.type == 'file';
|
||||
});
|
||||
|
||||
switch (_filePickerIndex) {
|
||||
@@ -1045,98 +1038,47 @@ class MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
|
||||
void _addAttachment(AssetEntity medium) async {
|
||||
final attachmentId = medium.id;
|
||||
_attachments[attachmentId] = _SendingAttachment(id: attachmentId);
|
||||
try {
|
||||
final mediaFile = await medium.originFile.timeout(
|
||||
Duration(seconds: 5),
|
||||
onTimeout: () => medium.originFile,
|
||||
);
|
||||
final mediaFile = await medium.originFile.timeout(
|
||||
Duration(seconds: 5),
|
||||
onTimeout: () => medium.originFile,
|
||||
);
|
||||
|
||||
var file = PlatformFile(
|
||||
path: mediaFile.path,
|
||||
size: ((await mediaFile.length()) / 1024).ceil(),
|
||||
bytes: mediaFile.readAsBytesSync(),
|
||||
);
|
||||
var file = AttachmentFile(
|
||||
path: mediaFile.path,
|
||||
size: await mediaFile.length(),
|
||||
bytes: mediaFile.readAsBytesSync(),
|
||||
);
|
||||
|
||||
if (file.size > _kMaxAttachmentSize) {
|
||||
if (medium?.type == AssetType.video) {
|
||||
final mediaInfo = await compressVideoService.compress(file.path);
|
||||
if (file.size > _kMaxAttachmentSize) {
|
||||
if (medium?.type == AssetType.video) {
|
||||
final mediaInfo = await VideoService.compressVideo(file.path);
|
||||
|
||||
if (mediaInfo.filesize / (1024 * 1024) > _kMaxAttachmentSize) {
|
||||
_showErrorAlert(
|
||||
'The file is too large to upload. The file size limit is 20MB. We tried compressing it, but it was not enough.',
|
||||
);
|
||||
_attachments.remove(attachmentId);
|
||||
return;
|
||||
}
|
||||
file = PlatformFile(
|
||||
name: file.name,
|
||||
size: (mediaInfo.filesize / 1024).ceil(),
|
||||
bytes: await mediaInfo.file.readAsBytes(),
|
||||
path: mediaInfo.path,
|
||||
);
|
||||
} else {
|
||||
if (mediaInfo.filesize > _kMaxAttachmentSize) {
|
||||
_showErrorAlert(
|
||||
'The file is too large to upload. The file size limit is 20MB.',
|
||||
'The file is too large to upload. The file size limit is 20MB. We tried compressing it, but it was not enough.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_attachments.update(attachmentId, (it) {
|
||||
return it.copyWith(
|
||||
file: file,
|
||||
attachment: Attachment(
|
||||
localUri: file.path != null ? Uri.parse(file.path) : null,
|
||||
type: medium?.type == AssetType.image ? 'image' : 'video',
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
final fileType = medium.type == AssetType.image
|
||||
? DefaultAttachmentTypes.image
|
||||
: DefaultAttachmentTypes.video;
|
||||
|
||||
final url = await _uploadAttachment(
|
||||
file,
|
||||
fileType,
|
||||
onSendProgress: (sent, total) {
|
||||
setState(() {
|
||||
_attachments.update(
|
||||
attachmentId,
|
||||
(it) => it.copyWith(totalUploaded: sent, totalSize: total),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
if (fileType == DefaultAttachmentTypes.image) {
|
||||
_attachments.update(attachmentId, (it) {
|
||||
return it.copyWith(attachment: it.attachment.copyWith(imageUrl: url));
|
||||
});
|
||||
file = AttachmentFile(
|
||||
name: file.name,
|
||||
size: mediaInfo.filesize,
|
||||
bytes: await mediaInfo.file.readAsBytes(),
|
||||
path: mediaInfo.path,
|
||||
);
|
||||
} else {
|
||||
_attachments.update(attachmentId, (it) {
|
||||
return it.copyWith(attachment: it.attachment.copyWith(assetUrl: url));
|
||||
});
|
||||
_showErrorAlert(
|
||||
'The file is too large to upload. The file size limit is 20MB.',
|
||||
);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
// Marking as upload complete
|
||||
_attachments.update(
|
||||
attachmentId,
|
||||
(it) => it.copyWith(totalUploaded: it.totalSize),
|
||||
);
|
||||
});
|
||||
}
|
||||
} catch (e, s) {
|
||||
setState(() => _attachments.remove(attachmentId));
|
||||
print(e);
|
||||
print(s);
|
||||
_showErrorAlert('Error adding the attachment: $e');
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_attachments[medium.id] = Attachment(
|
||||
id: medium.id,
|
||||
file: file,
|
||||
type: medium.type == AssetType.image ? 'image' : 'video',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildCommandIcon(String iconType) {
|
||||
@@ -1561,10 +1503,10 @@ class MessageInputState extends State<MessageInput> {
|
||||
Widget _buildAttachments() {
|
||||
if (_attachments.isEmpty) return Offstage();
|
||||
final fileAttachments = _attachments.values
|
||||
.where((it) => it.attachmentType == 'file')
|
||||
.where((it) => it.type == 'file')
|
||||
.toList(growable: false);
|
||||
final remainingAttachments = _attachments.values
|
||||
.where((it) => it.attachmentType != 'file')
|
||||
.where((it) => it.type != 'file')
|
||||
.toList(growable: false);
|
||||
return Column(
|
||||
children: [
|
||||
@@ -1582,37 +1524,20 @@ class MessageInputState extends State<MessageInput> {
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: FileAttachment(
|
||||
attachment: e.attachment,
|
||||
attachmentType: FileAttachmentType.local,
|
||||
file: e.file,
|
||||
message: null,
|
||||
attachment: e,
|
||||
size: Size(
|
||||
MediaQuery.of(context).size.width * 0.65,
|
||||
56.0,
|
||||
),
|
||||
trailing: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: InkWell(
|
||||
child: CircleAvatar(
|
||||
backgroundColor: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.6),
|
||||
maxRadius: 12.0,
|
||||
child: StreamSvgIcon.close(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.white,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
setState(() => _attachments.remove(e.id));
|
||||
},
|
||||
),
|
||||
child: _buildRemoveButton(e),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.insertBetween(const SizedBox(width: 8)),
|
||||
.insertBetween(const SizedBox(height: 8)),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1638,16 +1563,11 @@ class MessageInputState extends State<MessageInput> {
|
||||
child: _buildAttachment(attachment),
|
||||
),
|
||||
),
|
||||
_buildRemoveButton(attachment),
|
||||
if (!attachment.isUploaded)
|
||||
Positioned.fill(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: _buildRemoveButton(attachment),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1660,44 +1580,10 @@ class MessageInputState extends State<MessageInput> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUploadProgressIndicator(int uploaded, int total) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
Widget _buildRemoveButton(Attachment attachment) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: theme.colorTheme.overlayDark.withOpacity(0.6),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 5, bottom: 5, right: 11, left: 5),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 16,
|
||||
width: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
valueColor: AlwaysStoppedAnimation(Color(0xffb2b2b2)),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'${filesize(uploaded)} / ${filesize(total)}',
|
||||
style: theme.textTheme.footnote.copyWith(
|
||||
color: theme.colorTheme.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Positioned _buildRemoveButton(_SendingAttachment attachment) {
|
||||
return Positioned(
|
||||
height: 24,
|
||||
width: 24,
|
||||
top: 4,
|
||||
right: 4,
|
||||
child: RawMaterialButton(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
@@ -1721,21 +1607,18 @@ class MessageInputState extends State<MessageInput> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAttachment(_SendingAttachment attachment) {
|
||||
if (widget.attachmentThumbnailBuilders
|
||||
?.containsKey(attachment.attachmentType) ==
|
||||
Widget _buildAttachment(Attachment attachment) {
|
||||
if (attachment == null) return Offstage();
|
||||
|
||||
if (widget.attachmentThumbnailBuilders?.containsKey(attachment.type) ==
|
||||
true) {
|
||||
return widget.attachmentThumbnailBuilders[attachment.attachmentType](
|
||||
return widget.attachmentThumbnailBuilders[attachment.type](
|
||||
context,
|
||||
attachment,
|
||||
);
|
||||
}
|
||||
|
||||
if (attachment.attachment == null) {
|
||||
return SizedBox();
|
||||
}
|
||||
|
||||
switch (attachment.attachmentType) {
|
||||
switch (attachment.type) {
|
||||
case 'image':
|
||||
case 'giphy':
|
||||
return attachment.file != null
|
||||
@@ -1749,32 +1632,33 @@ class MessageInputState extends State<MessageInput> {
|
||||
);
|
||||
},
|
||||
)
|
||||
: Image.network(
|
||||
attachment.attachment.imageUrl,
|
||||
: CachedNetworkImage(
|
||||
imageUrl: attachment.imageUrl ??
|
||||
attachment.assetUrl ??
|
||||
attachment.thumbUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (_, obj, trace) {
|
||||
return getFileTypeImage(attachment.extraData['other']);
|
||||
},
|
||||
progressIndicatorBuilder: (context, _, progress) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
child: const CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
break;
|
||||
case 'video':
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
child: FutureBuilder<File>(
|
||||
future: VideoCompress.getFileThumbnail(attachment.file.path),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Image.asset(
|
||||
'images/placeholder.png',
|
||||
package: 'stream_chat_flutter',
|
||||
);
|
||||
}
|
||||
|
||||
return Image.file(
|
||||
snapshot.data,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
},
|
||||
),
|
||||
Container(
|
||||
height: 104,
|
||||
width: 104,
|
||||
child: VideoThumbnailImage(
|
||||
video: attachment.file.path,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
@@ -1787,7 +1671,6 @@ class MessageInputState extends State<MessageInput> {
|
||||
),
|
||||
],
|
||||
);
|
||||
break;
|
||||
default:
|
||||
return Container(
|
||||
child: Icon(Icons.insert_drive_file),
|
||||
@@ -1953,11 +1836,9 @@ class MessageInputState extends State<MessageInput> {
|
||||
/// Use this to add custom type attachments
|
||||
void addAttachment(Attachment attachment) {
|
||||
setState(() {
|
||||
final _attachment = _SendingAttachment(
|
||||
attachment: attachment,
|
||||
totalUploaded: attachment.extraData['file_size'],
|
||||
_attachments[attachment.id] = attachment.copyWith(
|
||||
uploadState: attachment.uploadState ?? UploadState.success(),
|
||||
);
|
||||
_attachments[_attachment.id] = _attachment;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1966,7 +1847,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
void pickFile(DefaultAttachmentTypes fileType, [bool camera = false]) async {
|
||||
setState(() => _inputEnabled = false);
|
||||
|
||||
PlatformFile file;
|
||||
AttachmentFile file;
|
||||
String attachmentType;
|
||||
|
||||
if (fileType == DefaultAttachmentTypes.image) {
|
||||
@@ -1988,8 +1869,8 @@ class MessageInputState extends State<MessageInput> {
|
||||
return;
|
||||
}
|
||||
final bytes = await pickedFile.readAsBytes();
|
||||
file = PlatformFile(
|
||||
size: (bytes.length / 1024).ceil(),
|
||||
file = AttachmentFile(
|
||||
size: bytes.length,
|
||||
path: pickedFile.path,
|
||||
bytes: bytes,
|
||||
);
|
||||
@@ -2007,7 +1888,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
withData: true,
|
||||
);
|
||||
if (res?.files?.isNotEmpty == true) {
|
||||
file = res.files.single;
|
||||
file = res.files.single.toAttachmentFile;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2017,7 +1898,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
final mimeType = file.path.split('/').last.mimeType;
|
||||
|
||||
var extraDataMap = <String, dynamic>{};
|
||||
final extraDataMap = <String, dynamic>{};
|
||||
|
||||
if (camera) {
|
||||
if (mimeType.type == 'video' || mimeType.type == 'image') {
|
||||
@@ -2035,97 +1916,46 @@ class MessageInputState extends State<MessageInput> {
|
||||
extraDataMap['file_size'] = file.size;
|
||||
}
|
||||
|
||||
final attachment = _SendingAttachment(
|
||||
final attachment = Attachment(
|
||||
file: file,
|
||||
attachment: Attachment(
|
||||
localUri: file.path != null ? Uri.parse(file.path) : null,
|
||||
type: attachmentType,
|
||||
extraData: extraDataMap.isNotEmpty ? extraDataMap : null,
|
||||
title: file.name,
|
||||
),
|
||||
type: attachmentType,
|
||||
extraData: extraDataMap.isNotEmpty ? extraDataMap : null,
|
||||
);
|
||||
final attachmentId = attachment.id;
|
||||
|
||||
setState(() => _attachments[attachmentId] = attachment);
|
||||
_attachments[attachment.id] = attachment;
|
||||
|
||||
if (file.size / 1024 > _kMaxAttachmentSize) {
|
||||
if (attachmentType == 'video') {
|
||||
final mediaInfo = await compressVideoService.compress(file.path);
|
||||
file = PlatformFile(
|
||||
name: mediaInfo.title,
|
||||
size: (mediaInfo.filesize / 1024).ceil(),
|
||||
if (file.size > _kMaxAttachmentSize) {
|
||||
if (attachmentType == 'Video') {
|
||||
final mediaInfo = await VideoService.compressVideo(file.path);
|
||||
|
||||
if (mediaInfo.filesize > _kMaxAttachmentSize) {
|
||||
_showErrorAlert(
|
||||
'The file is too large to upload. The file size limit is 20MB. We tried compressing it, but it was not enough.',
|
||||
);
|
||||
_attachments.remove(attachment.id);
|
||||
return;
|
||||
}
|
||||
file = AttachmentFile(
|
||||
name: file.name,
|
||||
size: mediaInfo.filesize,
|
||||
bytes: await mediaInfo.file.readAsBytes(),
|
||||
path: mediaInfo.path,
|
||||
);
|
||||
setState(() {
|
||||
_attachments.update(attachmentId, (it) => it.copyWith(file: file));
|
||||
});
|
||||
} else {
|
||||
_showErrorAlert(
|
||||
'The file is too large to upload. The file size limit is 20MB.',
|
||||
);
|
||||
setState(() => _attachments.remove(attachmentId));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
final url = await _uploadAttachment(
|
||||
file,
|
||||
fileType,
|
||||
onSendProgress: (sent, total) {
|
||||
setState(() {
|
||||
_attachments.update(
|
||||
attachmentId,
|
||||
(it) => it.copyWith(totalUploaded: sent, totalSize: total),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
if (fileType == DefaultAttachmentTypes.image) {
|
||||
_attachments.update(attachmentId, (it) {
|
||||
return it.copyWith(attachment: it.attachment.copyWith(imageUrl: url));
|
||||
});
|
||||
} else {
|
||||
_attachments.update(attachmentId, (it) {
|
||||
return it.copyWith(attachment: it.attachment.copyWith(assetUrl: url));
|
||||
});
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
// Marking as upload complete
|
||||
_attachments.update(
|
||||
attachmentId,
|
||||
(it) => it.copyWith(totalUploaded: it.totalSize),
|
||||
);
|
||||
});
|
||||
}
|
||||
} catch (e, s) {
|
||||
setState(() => _attachments.remove(attachmentId));
|
||||
print(e);
|
||||
print(s);
|
||||
_showErrorAlert('Error adding the attachment: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _uploadAttachment(
|
||||
PlatformFile file,
|
||||
DefaultAttachmentTypes type, {
|
||||
ProgressCallback onSendProgress,
|
||||
}) {
|
||||
if (type == DefaultAttachmentTypes.image) {
|
||||
return _attachmentUploader.uploadImage(
|
||||
file,
|
||||
onSendProgress: onSendProgress,
|
||||
);
|
||||
} else {
|
||||
return _attachmentUploader.uploadFile(
|
||||
file,
|
||||
onSendProgress: onSendProgress,
|
||||
);
|
||||
}
|
||||
setState(() {
|
||||
_attachments.update(attachment.id, (it) {
|
||||
return it.copyWith(
|
||||
file: file,
|
||||
extraData: {...it.extraData}..update('file_size', (_) => file.size),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildIdleSendButton(BuildContext context) {
|
||||
@@ -2203,7 +2033,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
if (widget.editMessage != null) {
|
||||
message = widget.editMessage.copyWith(
|
||||
text: text,
|
||||
attachments: _getAttachments(attachments).toList(),
|
||||
attachments: attachments,
|
||||
mentionedUsers:
|
||||
_mentionedUsers.where((u) => text.contains('@${u.name}')).toList(),
|
||||
);
|
||||
@@ -2211,7 +2041,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
message = (widget.initialMessage ?? Message()).copyWith(
|
||||
parentId: widget.parentMessage?.id,
|
||||
text: text,
|
||||
attachments: _getAttachments(attachments).toList(),
|
||||
attachments: attachments,
|
||||
mentionedUsers:
|
||||
_mentionedUsers.where((u) => text.contains('@${u.name}')).toList(),
|
||||
showInChannel: widget.parentMessage != null ? _sendAsDm : null,
|
||||
@@ -2237,13 +2067,11 @@ class MessageInputState extends State<MessageInput> {
|
||||
_mentionedUsers.clear();
|
||||
|
||||
if (widget.editMessage == null ||
|
||||
widget.editMessage.status == MessageSendingStatus.failed) {
|
||||
widget.editMessage.status == MessageSendingStatus.failed ||
|
||||
widget.editMessage.status == MessageSendingStatus.sending) {
|
||||
sendingFuture = channel.sendMessage(message);
|
||||
} else {
|
||||
sendingFuture = StreamChat.of(context).client.updateMessage(
|
||||
message,
|
||||
channel.cid,
|
||||
);
|
||||
sendingFuture = channel.updateMessage(message);
|
||||
}
|
||||
|
||||
return sendingFuture.then((resp) {
|
||||
@@ -2253,12 +2081,6 @@ class MessageInputState extends State<MessageInput> {
|
||||
});
|
||||
}
|
||||
|
||||
Iterable<Attachment> _getAttachments(List<_SendingAttachment> attachments) {
|
||||
return attachments.map((attachment) {
|
||||
return attachment.attachment;
|
||||
});
|
||||
}
|
||||
|
||||
StreamSubscription _keyboardListener;
|
||||
|
||||
void _showErrorAlert(String description) {
|
||||
@@ -2334,16 +2156,12 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
void _parseExistingMessage(Message message) {
|
||||
textEditingController.text = message.text;
|
||||
|
||||
_messageIsPresent = true;
|
||||
|
||||
message.attachments?.forEach((attachment) {
|
||||
final _attachment = _SendingAttachment(
|
||||
attachment: attachment,
|
||||
totalUploaded: attachment.extraData['file_size'],
|
||||
for (final attachment in message.attachments) {
|
||||
_attachments[attachment.id] = attachment.copyWith(
|
||||
uploadState: attachment.uploadState ?? UploadState.success(),
|
||||
);
|
||||
_attachments[_attachment.id] = _attachment;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2363,63 +2181,10 @@ class MessageInputState extends State<MessageInput> {
|
||||
FocusScope.of(context).requestFocus(_focusNode);
|
||||
_initialized = true;
|
||||
}
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
if (_attachmentUploader == null) {
|
||||
_attachmentUploader =
|
||||
widget.attachmentUploader ?? StreamAttachmentUploader(channel);
|
||||
} else if (_attachmentUploader is StreamAttachmentUploader) {
|
||||
_attachmentUploader = StreamAttachmentUploader(channel);
|
||||
}
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
}
|
||||
|
||||
class _SendingAttachment {
|
||||
_SendingAttachment({
|
||||
String id,
|
||||
this.file,
|
||||
this.attachment,
|
||||
this.totalUploaded = 0,
|
||||
int totalSize,
|
||||
}) : id = id ?? shortHash(DateTime.now().millisecondsSinceEpoch),
|
||||
attachmentType = attachment?.type,
|
||||
totalSize =
|
||||
totalSize ?? file?.size ?? attachment.extraData['file_size'];
|
||||
|
||||
final String id;
|
||||
final PlatformFile file;
|
||||
final Attachment attachment;
|
||||
final String attachmentType;
|
||||
|
||||
final int totalUploaded;
|
||||
final int totalSize;
|
||||
|
||||
// Progress while the attachment is uploading to the server
|
||||
// 0 -> 100
|
||||
double get uploadPercentage {
|
||||
if (totalSize == null) return null;
|
||||
return (totalUploaded / totalSize) * 100;
|
||||
}
|
||||
|
||||
bool get isUploaded => uploadPercentage == 100;
|
||||
|
||||
_SendingAttachment copyWith({
|
||||
String id,
|
||||
PlatformFile file,
|
||||
Attachment attachment,
|
||||
int totalUploaded,
|
||||
int totalSize,
|
||||
}) {
|
||||
return _SendingAttachment(
|
||||
id: id ?? this.id,
|
||||
file: file ?? this.file,
|
||||
attachment: attachment ?? this.attachment,
|
||||
totalUploaded: totalUploaded ?? this.totalUploaded,
|
||||
totalSize: totalSize ?? this.totalSize,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a 2-tuple, or pair.
|
||||
class Tuple2<T1, T2> {
|
||||
/// Returns the first item of the tuple
|
||||
|
||||
@@ -239,46 +239,50 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
|
||||
final MessageListController _messageListController = MessageListController();
|
||||
|
||||
final Map<String, VideoPackage> videoPackages = {};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MessageListCore(
|
||||
loadingBuilder: (context) {
|
||||
return Center(
|
||||
child: const CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
emptyBuilder: (context) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'No chats here yet...',
|
||||
style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5)),
|
||||
),
|
||||
);
|
||||
},
|
||||
messageListBuilder: (context, list) {
|
||||
return _buildListView(list);
|
||||
},
|
||||
messageListController: _messageListController,
|
||||
parentMessage: widget.parentMessage,
|
||||
showScrollToBottom: widget.showScrollToBottom,
|
||||
errorWidgetBuilder: (BuildContext context, Object error) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'Something went wrong',
|
||||
style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5)),
|
||||
),
|
||||
);
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
print('Getting popped');
|
||||
return false;
|
||||
},
|
||||
child: MessageListCore(
|
||||
loadingBuilder: (context) {
|
||||
return Center(
|
||||
child: const CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
emptyBuilder: (context) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'No chats here yet...',
|
||||
style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5)),
|
||||
),
|
||||
);
|
||||
},
|
||||
messageListBuilder: (context, list) {
|
||||
return _buildListView(list);
|
||||
},
|
||||
messageListController: _messageListController,
|
||||
parentMessage: widget.parentMessage,
|
||||
showScrollToBottom: widget.showScrollToBottom,
|
||||
errorWidgetBuilder: (BuildContext context, Object error) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'Something went wrong',
|
||||
style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5)),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -778,7 +782,6 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
break;
|
||||
}
|
||||
},
|
||||
videoPackages: videoPackages,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -939,10 +942,12 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
break;
|
||||
}
|
||||
},
|
||||
videoPackages: videoPackages,
|
||||
);
|
||||
|
||||
if (!message.isDeleted && !message.isSystem && !message.isEphemeral) {
|
||||
if (!message.isDeleted &&
|
||||
!message.isSystem &&
|
||||
!message.isEphemeral &&
|
||||
widget.onMessageSwiped != null) {
|
||||
child = Swipeable(
|
||||
onSwipeEnd: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
@@ -1056,7 +1061,6 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
streamChannel.reloadChannel();
|
||||
}
|
||||
_messageNewListener?.cancel();
|
||||
videoPackages.values.forEach((e) => e.dispose());
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
final ShapeBorder messageShape;
|
||||
final ShapeBorder attachmentShape;
|
||||
final void Function(User) onUserAvatarTap;
|
||||
final Map<String, VideoPackage> videoPackages;
|
||||
|
||||
const MessageReactionsModal({
|
||||
Key key,
|
||||
@@ -37,7 +36,6 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
this.reverse = false,
|
||||
this.showUserAvatar = DisplayWidget.show,
|
||||
this.onUserAvatarTap,
|
||||
this.videoPackages,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -146,7 +144,6 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
(message.status ==
|
||||
MessageSendingStatus.sent ||
|
||||
message.status == null),
|
||||
videoPackages: videoPackages,
|
||||
),
|
||||
),
|
||||
if (message.latestReactions?.isNotEmpty == true) ...[
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:stream_chat_flutter/src/reaction_bubble.dart';
|
||||
import 'package:stream_chat_flutter/src/url_attachment.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
import 'attachment/attachment.dart';
|
||||
import 'extension.dart';
|
||||
import 'image_group.dart';
|
||||
import 'message_text.dart';
|
||||
@@ -142,9 +143,6 @@ class MessageWidget extends StatefulWidget {
|
||||
/// Function called when quotedMessage is tapped
|
||||
final OnQuotedMessageTap onQuotedMessageTap;
|
||||
|
||||
/// The cache for the video controllers of attachments IDed as message ID + attachment index
|
||||
final Map<String, VideoPackage> videoPackages;
|
||||
|
||||
///
|
||||
MessageWidget({
|
||||
Key key,
|
||||
@@ -193,7 +191,6 @@ class MessageWidget extends StatefulWidget {
|
||||
this.attachmentPadding = EdgeInsets.zero,
|
||||
this.allRead = false,
|
||||
this.onQuotedMessageTap,
|
||||
this.videoPackages,
|
||||
}) : attachmentBuilders = {
|
||||
'image': (context, message, attachment) {
|
||||
return ImageAttachment(
|
||||
@@ -236,6 +233,7 @@ class MessageWidget extends StatefulWidget {
|
||||
},
|
||||
'file': (context, message, attachment) {
|
||||
return FileAttachment(
|
||||
message: message,
|
||||
attachment: attachment,
|
||||
size: Size(
|
||||
MediaQuery.of(context).size.width * 0.8,
|
||||
@@ -250,7 +248,8 @@ class MessageWidget extends StatefulWidget {
|
||||
_MessageWidgetState createState() => _MessageWidgetState();
|
||||
}
|
||||
|
||||
class _MessageWidgetState extends State<MessageWidget> {
|
||||
class _MessageWidgetState extends State<MessageWidget>
|
||||
with AutomaticKeepAliveClientMixin<MessageWidget> {
|
||||
bool get showThreadReplyIndicator => widget.showThreadReplyIndicator;
|
||||
|
||||
bool get showSendingIndicator => widget.showSendingIndicator;
|
||||
@@ -298,8 +297,12 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
showSendingIndicator ||
|
||||
isDeleted;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => widget.message.attachments?.isNotEmpty == true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
final avatarWidth = widget.messageTheme.avatarTheme.constraints.maxWidth;
|
||||
var leftPadding =
|
||||
widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5;
|
||||
@@ -744,7 +747,6 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
!isFailedState &&
|
||||
widget.onThreadTap != null,
|
||||
showFlagButton: widget.showFlagButton,
|
||||
videoPackages: widget.videoPackages,
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -773,7 +775,6 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
editMessageInputBuilder: widget.editMessageInputBuilder,
|
||||
onThreadTap: widget.onThreadTap,
|
||||
showReactions: widget.showReactions,
|
||||
videoPackages: widget.videoPackages,
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -824,6 +825,7 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
),
|
||||
images: images,
|
||||
message: widget.message,
|
||||
messageTheme: widget.messageTheme,
|
||||
onShowMessage: widget.onShowMessage,
|
||||
),
|
||||
),
|
||||
@@ -838,41 +840,6 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
children: widget.message.attachments
|
||||
?.where((element) => element.ogScrapeUrl == null)
|
||||
?.map((attachment) {
|
||||
if (attachment.type == 'video') {
|
||||
VideoPackage package;
|
||||
|
||||
if (widget.videoPackages == null) {
|
||||
package = VideoPackage(context, attachment, () {});
|
||||
} else {
|
||||
package = widget?.videoPackages[
|
||||
'${widget.message.id}${widget.message.attachments.indexOf(attachment)}'] ??
|
||||
VideoPackage(context, attachment, () {});
|
||||
}
|
||||
|
||||
if (widget.videoPackages != null) {
|
||||
widget.videoPackages[
|
||||
'${widget.message.id}${widget.message.attachments.indexOf(attachment)}'] =
|
||||
package;
|
||||
}
|
||||
|
||||
return Transform(
|
||||
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: VideoAttachment(
|
||||
attachment: attachment,
|
||||
messageTheme: widget.messageTheme,
|
||||
size: Size(
|
||||
MediaQuery.of(context).size.width * 0.8,
|
||||
MediaQuery.of(context).size.height * 0.3,
|
||||
),
|
||||
message: widget.message,
|
||||
onShowMessage: widget.onShowMessage,
|
||||
onReturnAction: widget.onReturnAction,
|
||||
videoPackage: package,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final attachmentBuilder =
|
||||
widget.attachmentBuilders[attachment.type];
|
||||
|
||||
@@ -885,7 +852,6 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
return wrapAttachmentWidget(
|
||||
context,
|
||||
attachmentWidget,
|
||||
attachment: attachment,
|
||||
);
|
||||
})?.insertBetween(SizedBox(
|
||||
height: widget.attachmentPadding.vertical / 2,
|
||||
@@ -897,9 +863,8 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
|
||||
Widget wrapAttachmentWidget(
|
||||
BuildContext context,
|
||||
Widget attachmentWidget, {
|
||||
Attachment attachment,
|
||||
}) {
|
||||
Widget attachmentWidget,
|
||||
) {
|
||||
final attachmentShape =
|
||||
widget.attachmentShape ?? _getDefaultAttachmentShape(context);
|
||||
return Material(
|
||||
@@ -930,8 +895,29 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
|
||||
Widget _buildSendingIndicator() {
|
||||
final style = widget.messageTheme.createdAt;
|
||||
final message = widget.message;
|
||||
|
||||
if (hasNonUrlAttachments &&
|
||||
(message.status == MessageSendingStatus.sending ||
|
||||
message.status == MessageSendingStatus.updating)) {
|
||||
final totalAttachments = message.attachments.length;
|
||||
final uploadRemaining = message.attachments.where((it) {
|
||||
return !it.uploadState.isSuccess;
|
||||
}).length;
|
||||
if (uploadRemaining == 0) {
|
||||
return StreamSvgIcon.check(
|
||||
size: style.fontSize,
|
||||
color: IconTheme.of(context).color.withOpacity(0.5),
|
||||
);
|
||||
}
|
||||
return Text(
|
||||
'Uploading $uploadRemaining/$totalAttachments ...',
|
||||
style: style,
|
||||
);
|
||||
}
|
||||
|
||||
Widget child = SendingIndicator(
|
||||
message: widget.message,
|
||||
message: message,
|
||||
isMessageRead: isMessageRead,
|
||||
size: style.fontSize,
|
||||
);
|
||||
@@ -1032,18 +1018,12 @@ class _MessageWidgetState extends State<MessageWidget> {
|
||||
return;
|
||||
}
|
||||
if (widget.message.status == MessageSendingStatus.failed_update) {
|
||||
StreamChat.of(context).client.updateMessage(
|
||||
widget.message,
|
||||
channel.cid,
|
||||
);
|
||||
channel.updateMessage(widget.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (widget.message.status == MessageSendingStatus.failed_delete) {
|
||||
StreamChat.of(context).client.deleteMessage(
|
||||
widget.message,
|
||||
channel.cid,
|
||||
);
|
||||
channel.deleteMessage(widget.message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import 'attachment_error.dart';
|
||||
import 'attachment/attachment.dart';
|
||||
import 'extension.dart';
|
||||
import 'image_attachment.dart';
|
||||
import 'message_text.dart';
|
||||
import 'stream_chat_theme.dart';
|
||||
import 'user_avatar.dart';
|
||||
@@ -200,10 +199,7 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
return AttachmentError(
|
||||
attachment: attachment,
|
||||
size: size,
|
||||
);
|
||||
return AttachmentError(size: size);
|
||||
}
|
||||
|
||||
Widget _parseAttachments(BuildContext context) {
|
||||
@@ -231,8 +227,8 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
transform: Matrix4.rotationY(reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: Material(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
color: Colors.transparent,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
type: MaterialType.transparency,
|
||||
shape: attachment.type == 'file' ? null : _getDefaultShape(context),
|
||||
child: child,
|
||||
),
|
||||
@@ -294,10 +290,9 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
},
|
||||
imageUrl:
|
||||
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl,
|
||||
errorWidget: (context, url, error) => AttachmentError(
|
||||
attachment: attachment,
|
||||
size: size,
|
||||
),
|
||||
errorWidget: (context, url, error) {
|
||||
return AttachmentError(size: size);
|
||||
},
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -901,4 +901,16 @@ class StreamSvgIcon extends StatelessWidget {
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.retry({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'icon_retry.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'stream_chat_theme.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
class UploadProgressIndicator extends StatelessWidget {
|
||||
final int uploaded;
|
||||
final int total;
|
||||
final Color progressIndicatorColor;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final bool showBackground;
|
||||
final TextStyle textStyle;
|
||||
|
||||
const UploadProgressIndicator({
|
||||
Key key,
|
||||
@required this.uploaded,
|
||||
@required this.total,
|
||||
this.progressIndicatorColor = const Color(0xffb2b2b2),
|
||||
this.padding = const EdgeInsets.only(top: 5, bottom: 5, right: 11, left: 5),
|
||||
this.showBackground = true,
|
||||
this.textStyle,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
Widget child = Padding(
|
||||
padding: padding,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 16,
|
||||
width: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
valueColor: AlwaysStoppedAnimation(progressIndicatorColor),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'${fileSize(uploaded, 1)}/${fileSize(total, 1)}',
|
||||
style: textStyle ??
|
||||
theme.textTheme.footnote.copyWith(
|
||||
color: theme.colorTheme.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (showBackground) {
|
||||
child = Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: theme.colorTheme.overlayDark.withOpacity(0.6),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'stream_svg_icon.dart';
|
||||
import 'dart:math';
|
||||
|
||||
Future<void> launchURL(BuildContext context, String url) async {
|
||||
if (await canLaunch(url)) {
|
||||
@@ -215,7 +216,7 @@ String getWebsiteName(String hostName) {
|
||||
}
|
||||
|
||||
/// A method returns a human readable string representing a file _size
|
||||
String filesize(dynamic size, [int round = 2]) {
|
||||
String fileSize(dynamic size, [int round = 2]) {
|
||||
if (size == null) return 'Size N/A';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/full_screen_media.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
import 'attachment_title.dart';
|
||||
|
||||
class VideoAttachment extends StatefulWidget {
|
||||
final Attachment attachment;
|
||||
final MessageTheme messageTheme;
|
||||
final Size size;
|
||||
final Message message;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
final ValueChanged<ReturnActionType> onReturnAction;
|
||||
final VideoPackage videoPackage;
|
||||
|
||||
VideoAttachment({
|
||||
Key key,
|
||||
@required this.attachment,
|
||||
@required this.messageTheme,
|
||||
this.videoPackage,
|
||||
this.message,
|
||||
this.size,
|
||||
this.onShowMessage,
|
||||
this.onReturnAction,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_VideoAttachmentState createState() => _VideoAttachmentState();
|
||||
}
|
||||
|
||||
class _VideoAttachmentState extends State<VideoAttachment> {
|
||||
bool initialized = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.videoPackage.onInit = () {
|
||||
setState(() {
|
||||
initialized = true;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!widget.videoPackage.initialised) {
|
||||
return Container(
|
||||
height: widget.size?.height ?? 100,
|
||||
width: widget.size?.width ?? 100,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
|
||||
var res = await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments: [widget.attachment],
|
||||
userName: widget.message.user.name,
|
||||
sentAt: widget.message.createdAt,
|
||||
message: widget.message,
|
||||
onShowMessage: widget.onShowMessage,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (res != null) {
|
||||
widget.onReturnAction(res);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
height: widget.size?.height,
|
||||
width: widget.size?.width,
|
||||
child: Flex(
|
||||
direction: Axis.vertical,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.none,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Chewie(
|
||||
controller: widget.videoPackage.chewieController,
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Center(
|
||||
child: Material(
|
||||
shape: CircleBorder(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Icon(Icons.play_arrow),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.attachment.title != null)
|
||||
Material(
|
||||
color: widget.messageTheme.messageBackgroundColor,
|
||||
child: AttachmentTitle(
|
||||
messageTheme: widget.messageTheme,
|
||||
attachment: widget.attachment,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:synchronized/synchronized.dart';
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
import 'package:video_thumbnail/video_thumbnail.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
class IVideoService {
|
||||
static final IVideoService instance = IVideoService._();
|
||||
final _lock = Lock();
|
||||
|
||||
IVideoService._();
|
||||
|
||||
/// compress video from [path]
|
||||
/// compress video from [path] return [Future<MediaInfo>]
|
||||
///
|
||||
/// you can choose its quality by [quality],
|
||||
/// determine whether to delete his source file by [deleteOrigin]
|
||||
/// optional parameters [startTime] [duration] [includeAudio] [frameRate]
|
||||
///
|
||||
/// ## example
|
||||
/// ```dart
|
||||
/// final info = await _flutterVideoCompress.compressVideo(
|
||||
/// file.path,
|
||||
/// deleteOrigin: true,
|
||||
/// );
|
||||
/// debugPrint(info.toJson());
|
||||
/// ```
|
||||
Future<MediaInfo> compressVideo(String path) async {
|
||||
return _lock.synchronized(() {
|
||||
return VideoCompress.compressVideo(
|
||||
path,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Generates a thumbnail image data in memory as UInt8List, it can be easily used by Image.memory(...).
|
||||
/// The video can be a local video file, or an URL repreents iOS or Android native supported video format.
|
||||
/// Speicify the maximum height or width for the thumbnail or 0 for same resolution as the original video.
|
||||
/// The lower quality value creates lower quality of the thumbnail image, but it gets ignored for PNG format.
|
||||
Future<Uint8List> generateVideoThumbnail({
|
||||
@required String video,
|
||||
ImageFormat imageFormat = ImageFormat.PNG,
|
||||
int maxHeight = 0,
|
||||
int maxWidth = 0,
|
||||
int timeMs = 0,
|
||||
int quality = 10,
|
||||
}) {
|
||||
return VideoThumbnail.thumbnailData(
|
||||
video: video,
|
||||
imageFormat: imageFormat,
|
||||
maxHeight: maxHeight,
|
||||
maxWidth: maxWidth,
|
||||
timeMs: timeMs,
|
||||
quality: quality,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: non_constant_identifier_names
|
||||
IVideoService get VideoService => IVideoService.instance;
|
||||
@@ -0,0 +1,87 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:video_thumbnail/video_thumbnail.dart';
|
||||
|
||||
import 'stream_svg_icon.dart';
|
||||
import 'video_service.dart';
|
||||
|
||||
class VideoThumbnailImage extends StatefulWidget {
|
||||
final String video;
|
||||
final double width;
|
||||
final double height;
|
||||
final BoxFit fit;
|
||||
final ImageFormat format;
|
||||
final Widget Function(BuildContext, Object) errorBuilder;
|
||||
final WidgetBuilder placeholderBuilder;
|
||||
|
||||
const VideoThumbnailImage({
|
||||
Key key,
|
||||
@required this.video,
|
||||
this.width,
|
||||
this.height,
|
||||
this.fit,
|
||||
this.format = ImageFormat.PNG,
|
||||
this.errorBuilder,
|
||||
this.placeholderBuilder,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_VideoThumbnailImageState createState() => _VideoThumbnailImageState();
|
||||
}
|
||||
|
||||
class _VideoThumbnailImageState extends State<VideoThumbnailImage> {
|
||||
Future<Uint8List> thumbnailFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
thumbnailFuture = VideoService.generateVideoThumbnail(
|
||||
video: widget.video,
|
||||
imageFormat: widget.format,
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant VideoThumbnailImage oldWidget) {
|
||||
if (oldWidget.video != widget.video || oldWidget.format != widget.format) {
|
||||
thumbnailFuture = VideoService.generateVideoThumbnail(
|
||||
video: widget.video,
|
||||
imageFormat: widget.format,
|
||||
);
|
||||
}
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<Uint8List>(
|
||||
future: thumbnailFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
if (widget.errorBuilder != null) {
|
||||
return widget.errorBuilder(context, snapshot.error);
|
||||
}
|
||||
return Center(child: StreamSvgIcon.error());
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
if (widget.placeholderBuilder != null) {
|
||||
return widget.placeholderBuilder(context);
|
||||
}
|
||||
return Image.asset(
|
||||
'images/placeholder.png',
|
||||
package: 'stream_chat_flutter',
|
||||
fit: widget.fit,
|
||||
);
|
||||
}
|
||||
final data = snapshot.data;
|
||||
return Image.memory(
|
||||
data,
|
||||
fit: widget.fit,
|
||||
height: widget.height,
|
||||
width: widget.width,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,10 @@ export 'src/channel_name.dart';
|
||||
export 'src/channel_preview.dart';
|
||||
export 'src/date_divider.dart';
|
||||
export 'src/deleted_message.dart';
|
||||
export 'src/file_attachment.dart';
|
||||
export 'src/attachment/attachment.dart';
|
||||
export 'src/full_screen_media.dart';
|
||||
export 'src/image_header.dart';
|
||||
export 'src/image_footer.dart';
|
||||
export 'src/giphy_attachment.dart';
|
||||
export 'src/image_attachment.dart';
|
||||
export 'src/message_input.dart';
|
||||
export 'src/message_list_view.dart';
|
||||
export 'src/message_text.dart';
|
||||
@@ -31,7 +29,6 @@ export 'src/user_item.dart';
|
||||
export 'src/user_list_view.dart';
|
||||
export 'src/user_list_view.dart';
|
||||
export 'src/utils.dart';
|
||||
export 'src/video_attachment.dart';
|
||||
export 'src/message_search_item.dart';
|
||||
export 'src/message_search_list_view.dart';
|
||||
export 'src/unread_indicator.dart';
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg width="24" height="19" viewBox="0 0 24 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M11.96 2C8.26563 2 5.20156 4.75636 4.71677 8.32007H6.31992C7.10992 8.32007 7.57992 9.21007 7.12992 9.87007L4.45992 13.7401C4.06992 14.3101 3.22992 14.3101 2.83992 13.7401L0.179922 9.87007C-0.280078 9.21007 0.189922 8.32007 0.989922 8.32007H2.70214C3.19783 3.6451 7.16369 0 11.96 0C13.76 0 15.5 0.51 17.01 1.49L15.92 3.17C14.74 2.4 13.37 2 11.96 2ZM6.90991 17.1201C8.41991 18.1001 10.1599 18.6101 11.9599 18.6101C16.7556 18.6101 20.7211 14.9659 21.2175 10.2999H22.9299C23.7299 10.2999 24.1999 9.40989 23.7499 8.74989L21.0799 4.87989C20.6899 4.30989 19.8499 4.30989 19.4599 4.87989L16.7899 8.74989C16.3399 9.40989 16.8099 10.2999 17.5999 10.2999H19.2031C18.7184 13.8637 15.6543 16.6201 11.9599 16.6201C10.5499 16.6201 9.17991 16.2101 7.99991 15.4501L6.90991 17.1201Z"
|
||||
fill="white" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 954 B |
@@ -47,6 +47,7 @@ dependencies:
|
||||
characters: ^1.0.0
|
||||
dio: ^3.0.10
|
||||
path_provider: ^1.6.27
|
||||
video_thumbnail: ^0.2.5+1
|
||||
|
||||
flutter:
|
||||
assets:
|
||||
|
||||
Reference in New Issue
Block a user