Move Flutter widgets to packages directory
This commit is contained in:
@@ -1,39 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.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,56 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
|
||||
import 'stream_chat_theme.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
class AttachmentTitle extends StatelessWidget {
|
||||
const AttachmentTitle({
|
||||
Key key,
|
||||
@required this.attachment,
|
||||
@required this.messageTheme,
|
||||
}) : super(key: key);
|
||||
|
||||
final MessageTheme messageTheme;
|
||||
final Attachment attachment;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (attachment.titleLink != null) {
|
||||
launchURL(context, attachment.titleLink);
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
attachment.title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: messageTheme.messageText.copyWith(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (attachment.titleLink != null || attachment.ogScrapeUrl != null)
|
||||
Text(
|
||||
Uri.parse(attachment.titleLink ?? attachment.ogScrapeUrl)
|
||||
.authority
|
||||
.split('.')
|
||||
.reversed
|
||||
.take(2)
|
||||
.toList()
|
||||
.reversed
|
||||
.join('.'),
|
||||
style: messageTheme.messageText,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/unread_indicator.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
|
||||
class StreamBackButton extends StatelessWidget {
|
||||
const StreamBackButton({
|
||||
Key key,
|
||||
this.onPressed,
|
||||
this.showUnreads = false,
|
||||
}) : super(key: key);
|
||||
|
||||
final VoidCallback onPressed;
|
||||
final bool showUnreads;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(14.0),
|
||||
child: RawMaterialButton(
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||
elevation: 0,
|
||||
highlightElevation: 0,
|
||||
focusElevation: 0,
|
||||
disabledElevation: 0,
|
||||
hoverElevation: 0,
|
||||
onPressed: () {
|
||||
if (onPressed != null) {
|
||||
onPressed();
|
||||
} else {
|
||||
Navigator.maybePop(context);
|
||||
}
|
||||
},
|
||||
child: StreamSvgIcon.left(
|
||||
size: 24,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showUnreads)
|
||||
Positioned(
|
||||
top: 7,
|
||||
right: 7,
|
||||
child: UnreadIndicator(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'channel_info.dart';
|
||||
import 'option_list_tile.dart';
|
||||
|
||||
class ChannelBottomSheet extends StatefulWidget {
|
||||
final VoidCallback onViewInfoTap;
|
||||
|
||||
ChannelBottomSheet({this.onViewInfoTap});
|
||||
|
||||
@override
|
||||
_ChannelBottomSheetState createState() => _ChannelBottomSheetState();
|
||||
}
|
||||
|
||||
class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var channel = StreamChannel.of(context).channel;
|
||||
|
||||
var members = channel.state.members;
|
||||
|
||||
var userAsMember =
|
||||
members.firstWhere((e) => e.user.id == StreamChat.of(context).user.id);
|
||||
var isOwner = userAsMember.role == 'owner';
|
||||
|
||||
return Material(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16.0),
|
||||
topRight: Radius.circular(16.0),
|
||||
),
|
||||
),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 24.0,
|
||||
),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: ChannelName(
|
||||
textStyle: StreamChatTheme.of(context).textTheme.headlineBold,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 5.0,
|
||||
),
|
||||
Center(
|
||||
child: ChannelInfo(
|
||||
showTypingIndicator: false,
|
||||
channel: StreamChannel.of(context).channel,
|
||||
textStyle:
|
||||
StreamChatTheme.of(context).channelPreviewTheme.subtitle,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 17.0,
|
||||
),
|
||||
if (channel.isDistinct && channel.memberCount == 2)
|
||||
Column(
|
||||
children: [
|
||||
UserAvatar(
|
||||
user: members
|
||||
.firstWhere((e) => e.user.id != userAsMember.user.id)
|
||||
.user,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: 64.0,
|
||||
maxWidth: 64.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(32.0),
|
||||
onlineIndicatorConstraints:
|
||||
BoxConstraints.tight(Size(16.0, 16.0)),
|
||||
),
|
||||
SizedBox(
|
||||
height: 6.0,
|
||||
),
|
||||
Text(
|
||||
members
|
||||
.firstWhere((e) => e.user.id != userAsMember.user.id)
|
||||
.user
|
||||
.name,
|
||||
style: StreamChatTheme.of(context).textTheme.footnoteBold,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!(channel.isDistinct && channel.memberCount == 2))
|
||||
Container(
|
||||
height: 94.0,
|
||||
alignment: Alignment.center,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: members.length,
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, index) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
UserAvatar(
|
||||
user: members[index].user,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: 64.0,
|
||||
maxWidth: 64.0,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(32.0),
|
||||
),
|
||||
SizedBox(
|
||||
height: 6.0,
|
||||
),
|
||||
Text(
|
||||
members[index].user.name,
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.footnoteBold,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 24.0,
|
||||
),
|
||||
OptionListTile(
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: StreamSvgIcon.user(
|
||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
),
|
||||
title: 'View Info',
|
||||
onTap: widget.onViewInfoTap,
|
||||
),
|
||||
if (!channel.isDistinct)
|
||||
OptionListTile(
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: StreamSvgIcon.userRemove(
|
||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
),
|
||||
title: 'Leave Group',
|
||||
onTap: () async {
|
||||
_showLeaveDialog();
|
||||
},
|
||||
),
|
||||
if (isOwner)
|
||||
OptionListTile(
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: StreamSvgIcon.delete(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
),
|
||||
),
|
||||
title: 'Delete Conversation',
|
||||
titleColor: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
onTap: () async {
|
||||
_showDeleteDialog();
|
||||
},
|
||||
),
|
||||
OptionListTile(
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: StreamSvgIcon.close_small(
|
||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
),
|
||||
title: 'Cancel',
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDeleteDialog() async {
|
||||
final res = await showConfirmationDialog(
|
||||
context,
|
||||
title: 'Delete Conversation',
|
||||
okText: 'DELETE',
|
||||
question: 'Are you sure you want to delete this conversation?',
|
||||
cancelText: 'CANCEL',
|
||||
icon: StreamSvgIcon.delete(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
),
|
||||
);
|
||||
var channel = StreamChannel.of(context).channel;
|
||||
if (res == true) {
|
||||
await channel.delete();
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
|
||||
void _showLeaveDialog() async {
|
||||
final res = await showConfirmationDialog(
|
||||
context,
|
||||
title: 'Leave conversation',
|
||||
okText: 'LEAVE',
|
||||
question: 'Are you sure you want to leave this conversation?',
|
||||
cancelText: 'CANCEL',
|
||||
icon: StreamSvgIcon.userRemove(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
),
|
||||
);
|
||||
var channel = StreamChannel.of(context).channel;
|
||||
if (res == true) {
|
||||
await channel.removeMembers([StreamChat.of(context).user.id]);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.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.
|
||||
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
|
||||
/// Direction can be ascending or descending.
|
||||
final List<SortOption> sortOptions;
|
||||
|
||||
/// Pagination parameters
|
||||
/// limit: the number of users to return (max is 30)
|
||||
/// offset: the offset (max is 1000)
|
||||
/// message_limit: how many messages should be included to each channel
|
||||
final PaginationParams paginationParams;
|
||||
|
||||
/// The builder used when the file list is empty.
|
||||
final WidgetBuilder emptyBuilder;
|
||||
|
||||
const ChannelFileDisplayScreen({
|
||||
this.sortOptions,
|
||||
this.paginationParams,
|
||||
this.emptyBuilder,
|
||||
});
|
||||
|
||||
@override
|
||||
_ChannelFileDisplayScreenState createState() =>
|
||||
_ChannelFileDisplayScreenState();
|
||||
}
|
||||
|
||||
class _ChannelFileDisplayScreenState extends State<ChannelFileDisplayScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final messageSearchBloc = MessageSearchBloc.of(context);
|
||||
messageSearchBloc.search(
|
||||
filter: {
|
||||
'cid': {
|
||||
r'$in': ['messaging:${StreamChannel.of(context).channel.id}']
|
||||
}
|
||||
},
|
||||
messageFilter: {
|
||||
'attachments.type': {
|
||||
r'$in': ['file'],
|
||||
},
|
||||
},
|
||||
sort: widget.sortOptions,
|
||||
pagination: widget.paginationParams,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||
appBar: AppBar(
|
||||
brightness: Theme.of(context).brightness,
|
||||
elevation: 1,
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
'Files',
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
fontSize: 16.0),
|
||||
),
|
||||
leading: Center(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Container(
|
||||
child: StreamSvgIcon.left(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
size: 24.0,
|
||||
),
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||
),
|
||||
body: _buildMediaGrid(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMediaGrid() {
|
||||
final messageSearchBloc = MessageSearchBloc.of(context);
|
||||
|
||||
return StreamBuilder<List<GetMessageResponse>>(
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.data == null) {
|
||||
return Center(
|
||||
child: const CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot.data.isEmpty) {
|
||||
if (widget.emptyBuilder != null) {
|
||||
return widget.emptyBuilder(context);
|
||||
}
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
StreamSvgIcon.files(
|
||||
size: 136.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
),
|
||||
SizedBox(height: 16.0),
|
||||
Text(
|
||||
'No Files',
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.0),
|
||||
Text(
|
||||
'Files sent in this chat will appear here',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final media = <Attachment, Message>{};
|
||||
|
||||
for (var item in snapshot.data) {
|
||||
item.message.attachments.where((e) => e.type == 'file').forEach((e) {
|
||||
media[e] = item.message;
|
||||
});
|
||||
}
|
||||
|
||||
return LazyLoadScrollView(
|
||||
onEndOfPage: () => messageSearchBloc.search(
|
||||
filter: {
|
||||
'cid': {
|
||||
r'$in': ['messaging:${StreamChannel.of(context).channel.id}']
|
||||
}
|
||||
},
|
||||
messageFilter: {
|
||||
'attachments.type': {
|
||||
r'$in': ['file']
|
||||
},
|
||||
},
|
||||
sort: widget.sortOptions,
|
||||
pagination: widget.paginationParams.copyWith(
|
||||
offset: messageSearchBloc.messageResponses?.length ?? 0,
|
||||
),
|
||||
),
|
||||
child: ListView.builder(
|
||||
itemBuilder: (context, position) {
|
||||
var channel = StreamChannel.of(context).channel;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(1.0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: FileAttachment(
|
||||
attachment: media.keys.toList()[position],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: media.length,
|
||||
),
|
||||
);
|
||||
},
|
||||
stream: messageSearchBloc.messagesStream,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/back_button.dart';
|
||||
import 'package:stream_chat_flutter/src/channel_info.dart';
|
||||
import 'package:stream_chat_flutter/src/channel_name.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import './channel_name.dart';
|
||||
import 'channel_image.dart';
|
||||
import 'stream_channel.dart';
|
||||
|
||||
/// 
|
||||
/// 
|
||||
///
|
||||
/// It shows the current [Channel] information.
|
||||
///
|
||||
/// ```dart
|
||||
/// class MyApp extends StatelessWidget {
|
||||
/// final Client client;
|
||||
/// final Channel channel;
|
||||
///
|
||||
/// MyApp(this.client, this.channel);
|
||||
///
|
||||
/// @override
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return MaterialApp(
|
||||
/// home: StreamChat(
|
||||
/// client: client,
|
||||
/// child: StreamChannel(
|
||||
/// channel: channel,
|
||||
/// child: Scaffold(
|
||||
/// appBar: ChannelHeader(),
|
||||
/// ),
|
||||
/// ),
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Usually you would use this widget as an [AppBar] inside a [Scaffold].
|
||||
/// However you can also use it as a normal widget.
|
||||
///
|
||||
/// Make sure to have a [StreamChannel] ancestor in order to provide the information about the channel.
|
||||
/// Every part of the widget uses a [StreamBuilder] to render the channel information as soon as it updates.
|
||||
///
|
||||
/// By default the widget shows a backButton that calls [Navigator.pop].
|
||||
/// You can disable this button using the [showBackButton] property of just override the behaviour
|
||||
/// with [onBackPressed].
|
||||
///
|
||||
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property.
|
||||
/// Modify it to change the widget appearance.
|
||||
class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
/// True if this header shows the leading back button
|
||||
final bool showBackButton;
|
||||
|
||||
/// Callback to call when pressing the back button.
|
||||
/// By default it calls [Navigator.pop]
|
||||
final VoidCallback onBackPressed;
|
||||
|
||||
/// Callback to call when the header is tapped.
|
||||
final VoidCallback onTitleTap;
|
||||
|
||||
/// Callback to call when the image is tapped.
|
||||
final VoidCallback onImageTap;
|
||||
|
||||
/// If true the typing indicator will be rendered if a user is typing
|
||||
final bool showTypingIndicator;
|
||||
|
||||
/// Creates a channel header
|
||||
ChannelHeader({
|
||||
Key key,
|
||||
this.showBackButton = true,
|
||||
this.onBackPressed,
|
||||
this.onTitleTap,
|
||||
this.showTypingIndicator = true,
|
||||
this.onImageTap,
|
||||
}) : preferredSize = Size.fromHeight(kToolbarHeight),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
return AppBar(
|
||||
brightness: Theme.of(context).brightness,
|
||||
elevation: 1,
|
||||
leading: showBackButton
|
||||
? StreamBackButton(
|
||||
onPressed: onBackPressed,
|
||||
showUnreads: true,
|
||||
)
|
||||
: SizedBox(),
|
||||
backgroundColor:
|
||||
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
|
||||
actions: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 10.0),
|
||||
child: Center(
|
||||
child: ChannelImage(
|
||||
onTap: onImageTap,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
centerTitle: true,
|
||||
title: InkWell(
|
||||
onTap: onTitleTap,
|
||||
child: Container(
|
||||
height: preferredSize.height,
|
||||
width: preferredSize.width,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
ChannelName(
|
||||
textStyle: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.title,
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
ChannelInfo(
|
||||
showTypingIndicator: showTypingIndicator,
|
||||
channel: channel,
|
||||
textStyle:
|
||||
StreamChatTheme.of(context).channelPreviewTheme.subtitle,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
final Size preferredSize;
|
||||
}
|
||||
@@ -1,214 +0,0 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/group_image.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// 
|
||||
/// 
|
||||
///
|
||||
/// It shows the current [Channel] image.
|
||||
///
|
||||
/// ```dart
|
||||
/// class MyApp extends StatelessWidget {
|
||||
/// final Client client;
|
||||
/// final Channel channel;
|
||||
///
|
||||
/// MyApp(this.client, this.channel);
|
||||
///
|
||||
/// @override
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return MaterialApp(
|
||||
/// debugShowCheckedModeBanner: false,
|
||||
/// home: StreamChat(
|
||||
/// client: client,
|
||||
/// child: StreamChannel(
|
||||
/// channel: channel,
|
||||
/// child: Center(
|
||||
/// child: ChannelImage(
|
||||
/// channel: channel,
|
||||
/// ),
|
||||
/// ),
|
||||
/// ),
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The widget uses a [StreamBuilder] to render the channel information image as soon as it updates.
|
||||
///
|
||||
/// By default the widget radius size is 40x40 pixels.
|
||||
/// Set the property [constraints] to set a custom dimension.
|
||||
///
|
||||
/// The widget renders the ui based on the first ancestor of type [StreamChatTheme].
|
||||
/// Modify it to change the widget appearance.
|
||||
class ChannelImage extends StatelessWidget {
|
||||
/// Instantiate a new ChannelImage
|
||||
const ChannelImage({
|
||||
Key key,
|
||||
this.channel,
|
||||
this.constraints,
|
||||
this.onTap,
|
||||
this.showOnlineStatus = true,
|
||||
this.borderRadius,
|
||||
this.selected = false,
|
||||
this.selectionColor,
|
||||
this.selectionThickness = 4,
|
||||
}) : super(key: key);
|
||||
|
||||
final BorderRadius borderRadius;
|
||||
|
||||
/// The channel to show the image of
|
||||
final Channel channel;
|
||||
|
||||
/// The diameter of the image
|
||||
final BoxConstraints constraints;
|
||||
|
||||
/// The function called when the image is tapped
|
||||
final VoidCallback onTap;
|
||||
|
||||
final bool showOnlineStatus;
|
||||
|
||||
final bool selected;
|
||||
|
||||
final Color selectionColor;
|
||||
|
||||
final double selectionThickness;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final streamChat = StreamChat.of(context);
|
||||
final channel = this.channel ?? StreamChannel.of(context).channel;
|
||||
return StreamBuilder<Map<String, dynamic>>(
|
||||
stream: channel.extraDataStream,
|
||||
initialData: channel.extraData,
|
||||
builder: (context, snapshot) {
|
||||
String image;
|
||||
if (snapshot.data?.containsKey('image') == true) {
|
||||
image = snapshot.data['image'];
|
||||
} else if (channel.state.members?.length == 2) {
|
||||
final otherMember = channel.state.members
|
||||
.firstWhere((member) => member.user.id != streamChat.user.id);
|
||||
return StreamBuilder<User>(
|
||||
stream: streamChat.client.state.usersStream
|
||||
.map((users) => users[otherMember.userId]),
|
||||
initialData: otherMember.user,
|
||||
builder: (context, snapshot) {
|
||||
return UserAvatar(
|
||||
borderRadius: borderRadius,
|
||||
user: snapshot.data ?? otherMember.user,
|
||||
constraints: constraints ??
|
||||
StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.avatarTheme
|
||||
.constraints,
|
||||
onTap: onTap != null ? (_) => onTap() : null,
|
||||
selected: selected,
|
||||
selectionColor: selectionColor ??
|
||||
StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
selectionThickness: selectionThickness,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
final images = channel.state.members
|
||||
.where((member) =>
|
||||
member.user.id != streamChat.user.id &&
|
||||
member.user.extraData['image'] != null)
|
||||
.take(4)
|
||||
.map((e) => e.user.extraData['image'] as String)
|
||||
.toList();
|
||||
return GroupImage(
|
||||
images: images,
|
||||
borderRadius: borderRadius,
|
||||
constraints: constraints ??
|
||||
StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.avatarTheme
|
||||
.constraints,
|
||||
onTap: onTap,
|
||||
selected: selected,
|
||||
selectionColor: selectionColor ??
|
||||
StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
selectionThickness: selectionThickness,
|
||||
);
|
||||
}
|
||||
|
||||
Widget child = ClipRRect(
|
||||
borderRadius: borderRadius ??
|
||||
StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.avatarTheme
|
||||
.borderRadius,
|
||||
child: Container(
|
||||
constraints: constraints ??
|
||||
StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.avatarTheme
|
||||
.constraints,
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
fit: StackFit.expand,
|
||||
children: <Widget>[
|
||||
image != null
|
||||
? CachedNetworkImage(
|
||||
imageUrl: image,
|
||||
errorWidget: (_, __, ___) {
|
||||
return Center(
|
||||
child: Text(
|
||||
snapshot.data?.containsKey('name') ?? false
|
||||
? snapshot.data['name'][0]
|
||||
: '',
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: StreamChatTheme.of(context)
|
||||
.defaultChannelImage(context, channel),
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
if (selected) {
|
||||
child = ClipRRect(
|
||||
borderRadius: (borderRadius ??
|
||||
StreamChatTheme.of(context)
|
||||
.ownMessageTheme
|
||||
.avatarTheme
|
||||
.borderRadius) +
|
||||
BorderRadius.circular(selectionThickness),
|
||||
child: Container(
|
||||
constraints: constraints ??
|
||||
StreamChatTheme.of(context)
|
||||
.ownMessageTheme
|
||||
.avatarTheme
|
||||
.constraints,
|
||||
color: selectionColor ??
|
||||
StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(selectionThickness),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return child;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
class ChannelInfo extends StatelessWidget {
|
||||
final Channel channel;
|
||||
|
||||
/// The style of the text displayed
|
||||
final TextStyle textStyle;
|
||||
|
||||
/// If true the typing indicator will be rendered if a user is typing
|
||||
final bool showTypingIndicator;
|
||||
|
||||
const ChannelInfo({
|
||||
Key key,
|
||||
@required this.channel,
|
||||
this.textStyle,
|
||||
this.showTypingIndicator = true,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final client = StreamChat.of(context).client;
|
||||
return StreamBuilder<List<Member>>(
|
||||
stream: channel.state.membersStream,
|
||||
initialData: channel.state.members,
|
||||
builder: (context, snapshot) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: client.wsConnectionStatus,
|
||||
builder: (context, status, child) {
|
||||
switch (status) {
|
||||
case ConnectionStatus.connected:
|
||||
return _buildConnectedTitleState(context, snapshot.data);
|
||||
case ConnectionStatus.connecting:
|
||||
return _buildConnectingTitleState(context);
|
||||
case ConnectionStatus.disconnected:
|
||||
return _buildDisconnectedTitleState(context, client);
|
||||
default:
|
||||
return Offstage();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildConnectedTitleState(BuildContext context, List<Member> members) {
|
||||
var alternativeWidget;
|
||||
|
||||
if (channel.memberCount != null && channel.memberCount > 2) {
|
||||
alternativeWidget = Text(
|
||||
'${channel.memberCount} Members, ${channel.state.watcherCount} Online',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.lastMessageAt,
|
||||
);
|
||||
} else {
|
||||
final otherMember = members.firstWhere(
|
||||
(element) => element.userId != StreamChat.of(context).user.id,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
if (otherMember != null) {
|
||||
if (otherMember.user.online) {
|
||||
alternativeWidget = Text(
|
||||
'Online',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.lastMessageAt,
|
||||
);
|
||||
} else {
|
||||
alternativeWidget = Text(
|
||||
'Last seen ${Jiffy(otherMember.user.lastActive).fromNow()}',
|
||||
style: textStyle,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!showTypingIndicator) {
|
||||
return alternativeWidget ?? Offstage();
|
||||
}
|
||||
|
||||
return TypingIndicator(
|
||||
alignment: Alignment.center,
|
||||
alternativeWidget: alternativeWidget,
|
||||
style: textStyle,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildConnectingTitleState(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
height: 16,
|
||||
width: 16,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Text(
|
||||
'Searching for Network',
|
||||
style: textStyle,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDisconnectedTitleState(BuildContext context, Client client) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Offline...',
|
||||
style: textStyle,
|
||||
),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.all(0),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
visualDensity: VisualDensity(
|
||||
horizontal: VisualDensity.minimumDensity,
|
||||
vertical: VisualDensity.minimumDensity,
|
||||
),
|
||||
),
|
||||
onPressed: () async {
|
||||
await client.disconnect();
|
||||
return client.connect();
|
||||
},
|
||||
child: Text(
|
||||
'Try Again',
|
||||
style: textStyle.copyWith(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_neumorphic_button.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
import 'stream_chat.dart';
|
||||
|
||||
typedef _TitleBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
ConnectionStatus status,
|
||||
Client client,
|
||||
);
|
||||
|
||||
///
|
||||
/// It shows the current [Client] status.
|
||||
///
|
||||
/// ```dart
|
||||
/// class MyApp extends StatelessWidget {
|
||||
/// final Client client;
|
||||
///
|
||||
/// MyApp(this.client);
|
||||
///
|
||||
/// @override
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return MaterialApp(
|
||||
/// home: StreamChat(
|
||||
/// client: client,
|
||||
/// child: Scaffold(
|
||||
/// appBar: ChannelListHeader(),
|
||||
/// ),
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Usually you would use this widget as an [AppBar] inside a [Scaffold].
|
||||
/// However you can also use it as a normal widget.
|
||||
///
|
||||
/// The widget by default uses the inherited [Client] to fetch information about the status.
|
||||
/// However you can also pass your own [Client] if you don't have it in the widget tree.
|
||||
///
|
||||
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property.
|
||||
/// Modify it to change the widget appearance.
|
||||
class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
/// Instantiates a ChannelListHeader
|
||||
const ChannelListHeader({
|
||||
Key key,
|
||||
this.client,
|
||||
this.titleBuilder,
|
||||
this.onUserAvatarTap,
|
||||
this.onNewChatButtonTap,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Pass this if you don't have a [Client] in your widget tree.
|
||||
final Client client;
|
||||
|
||||
/// Use this to build your own title as per different [ConnectionStatus]
|
||||
final _TitleBuilder titleBuilder;
|
||||
|
||||
/// Callback to call when pressing the user avatar button.
|
||||
/// By default it calls Scaffold.of(context).openDrawer()
|
||||
final Function(User) onUserAvatarTap;
|
||||
|
||||
/// Callback to call when pressing the new chat button.
|
||||
final VoidCallback onNewChatButtonTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _client = client ?? StreamChat.of(context).client;
|
||||
final user = _client.state.user;
|
||||
return AppBar(
|
||||
brightness: Theme.of(context).brightness,
|
||||
elevation: 1,
|
||||
backgroundColor:
|
||||
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
|
||||
centerTitle: true,
|
||||
leading: Center(
|
||||
child: UserAvatar(
|
||||
user: user,
|
||||
showOnlineStatus: false,
|
||||
onTap: onUserAvatarTap ?? (_) => Scaffold.of(context).openDrawer(),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 40,
|
||||
width: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
StreamNeumorphicButton(
|
||||
child: IconButton(
|
||||
icon: ValueListenableBuilder<ConnectionStatus>(
|
||||
valueListenable: _client.wsConnectionStatus,
|
||||
builder: (context, status, child) {
|
||||
var color;
|
||||
switch (status) {
|
||||
case ConnectionStatus.connected:
|
||||
color = StreamChatTheme.of(context).colorTheme.accentBlue;
|
||||
break;
|
||||
case ConnectionStatus.connecting:
|
||||
color = Colors.grey;
|
||||
break;
|
||||
case ConnectionStatus.disconnected:
|
||||
color = Colors.grey;
|
||||
break;
|
||||
}
|
||||
return SvgPicture.asset(
|
||||
'svgs/icon_pen_write.svg',
|
||||
package: 'stream_chat_flutter',
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
color: color,
|
||||
);
|
||||
},
|
||||
),
|
||||
onPressed: onNewChatButtonTap,
|
||||
),
|
||||
)
|
||||
],
|
||||
title: ValueListenableBuilder<ConnectionStatus>(
|
||||
valueListenable: _client.wsConnectionStatus,
|
||||
builder: (context, status, child) {
|
||||
if (titleBuilder != null) {
|
||||
return titleBuilder(context, status, _client);
|
||||
}
|
||||
switch (status) {
|
||||
case ConnectionStatus.connected:
|
||||
return _buildConnectedTitleState(context);
|
||||
case ConnectionStatus.connecting:
|
||||
return _buildConnectingTitleState(context);
|
||||
case ConnectionStatus.disconnected:
|
||||
return _buildDisconnectedTitleState(context, _client);
|
||||
default:
|
||||
return Offstage();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildConnectedTitleState(BuildContext context) => Text(
|
||||
'Stream Chat',
|
||||
style: StreamChatTheme.of(context).textTheme.headlineBold.copyWith(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _buildConnectingTitleState(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
height: 16,
|
||||
width: 16,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Text(
|
||||
'Searching for Network',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.title
|
||||
.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDisconnectedTitleState(BuildContext context, Client client) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Offline...',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.title
|
||||
.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await client.disconnect();
|
||||
return client.connect();
|
||||
},
|
||||
child: Text(
|
||||
'Try Again',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.title
|
||||
.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => Size.fromHeight(kToolbarHeight);
|
||||
}
|
||||
@@ -1,829 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/channels_bloc.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/utils.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'channel_bottom_sheet.dart';
|
||||
import 'channel_preview.dart';
|
||||
import 'stream_channel.dart';
|
||||
import 'stream_chat.dart';
|
||||
|
||||
/// Callback called when tapping on a channel
|
||||
typedef ChannelTapCallback = void Function(Channel, Widget);
|
||||
|
||||
/// Builder used to create a custom [ChannelPreview] from a [Channel]
|
||||
typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel);
|
||||
|
||||
typedef ViewInfoCallback = void Function(Channel);
|
||||
|
||||
/// 
|
||||
/// 
|
||||
///
|
||||
/// It shows the list of current channels.
|
||||
///
|
||||
/// ```dart
|
||||
/// class ChannelListPage extends StatelessWidget {
|
||||
/// @override
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return Scaffold(
|
||||
/// body: ChannelListView(
|
||||
/// filter: {
|
||||
/// 'members': {
|
||||
/// '\$in': [StreamChat.of(context).user.id],
|
||||
/// }
|
||||
/// },
|
||||
/// sort: [SortOption('last_message_at')],
|
||||
/// pagination: PaginationParams(
|
||||
/// limit: 20,
|
||||
/// ),
|
||||
/// channelWidget: ChannelPage(),
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
/// Make sure to have a [StreamChat] ancestor in order to provide the information about the channels.
|
||||
/// The widget uses a [ListView.custom] to render the list of channels.
|
||||
///
|
||||
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme].
|
||||
/// Modify it to change the widget appearance.
|
||||
class ChannelListView extends StatefulWidget {
|
||||
/// Instantiate a new ChannelListView
|
||||
ChannelListView({
|
||||
Key key,
|
||||
this.filter,
|
||||
this.options,
|
||||
this.sort,
|
||||
this.pagination,
|
||||
this.onChannelTap,
|
||||
this.onChannelLongPress,
|
||||
this.channelWidget,
|
||||
this.channelPreviewBuilder,
|
||||
this.separatorBuilder,
|
||||
this.errorBuilder,
|
||||
this.emptyBuilder,
|
||||
this.onImageTap,
|
||||
this.onStartChatPressed,
|
||||
this.swipeToAction = false,
|
||||
this.pullToRefresh = true,
|
||||
this.crossAxisCount = 1,
|
||||
this.padding,
|
||||
this.selectedChannels = const [],
|
||||
this.onViewInfoTap,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The builder that will be used in case of error
|
||||
final Widget Function(Error error) errorBuilder;
|
||||
|
||||
/// If true a default swipe to action behaviour will be added to this widget
|
||||
final bool swipeToAction;
|
||||
|
||||
/// The builder used when the channel list is empty.
|
||||
final WidgetBuilder emptyBuilder;
|
||||
|
||||
/// The query filters to use.
|
||||
/// You can query on any of the custom fields you've defined on the [Channel].
|
||||
/// You can also filter other built-in channel fields.
|
||||
final Map<String, dynamic> filter;
|
||||
|
||||
/// Query channels options.
|
||||
///
|
||||
/// state: if true returns the Channel state
|
||||
/// watch: if true listen to changes to this Channel in real time.
|
||||
final Map<String, dynamic> options;
|
||||
|
||||
/// The sorting used for the channels matching the filters.
|
||||
/// Sorting is based on field and direction, multiple sorting options can be provided.
|
||||
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
|
||||
/// Direction can be ascending or descending.
|
||||
final List<SortOption> sort;
|
||||
|
||||
/// Pagination parameters
|
||||
/// limit: the number of channels to return (max is 30)
|
||||
/// offset: the offset (max is 1000)
|
||||
/// message_limit: how many messages should be included to each channel
|
||||
final PaginationParams pagination;
|
||||
|
||||
/// Function called when tapping on a channel
|
||||
/// By default it calls [Navigator.push] building a [MaterialPageRoute]
|
||||
/// with the widget [channelWidget] as child.
|
||||
final ChannelTapCallback onChannelTap;
|
||||
|
||||
/// Function called when long pressing on a channel
|
||||
final Function(Channel) onChannelLongPress;
|
||||
|
||||
/// Widget used when opening a channel
|
||||
final Widget channelWidget;
|
||||
|
||||
/// Builder used to create a custom channel preview
|
||||
final ChannelPreviewBuilder channelPreviewBuilder;
|
||||
|
||||
/// Builder used to create a custom item separator
|
||||
final Function(BuildContext, int) separatorBuilder;
|
||||
|
||||
/// The function called when the image is tapped
|
||||
final Function(Channel) onImageTap;
|
||||
|
||||
/// Set it to false to disable the pull-to-refresh widget
|
||||
final bool pullToRefresh;
|
||||
|
||||
/// Callback used in the default empty list widget
|
||||
final VoidCallback onStartChatPressed;
|
||||
|
||||
/// The number of children in the cross axis.
|
||||
final int crossAxisCount;
|
||||
|
||||
/// The amount of space by which to inset the children.
|
||||
final EdgeInsetsGeometry padding;
|
||||
|
||||
final List<Channel> selectedChannels;
|
||||
|
||||
final ViewInfoCallback onViewInfoTap;
|
||||
|
||||
@override
|
||||
_ChannelListViewState createState() => _ChannelListViewState();
|
||||
}
|
||||
|
||||
class _ChannelListViewState extends State<ChannelListView>
|
||||
with WidgetsBindingObserver {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final SlidableController _slideController = SlidableController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final channelsBloc = ChannelsBloc.of(context);
|
||||
|
||||
if (!widget.pullToRefresh) {
|
||||
return _buildListView(channelsBloc);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
return channelsBloc.queryChannels(
|
||||
filter: widget.filter,
|
||||
sortOptions: widget.sort,
|
||||
paginationParams: widget.pagination,
|
||||
options: widget.options,
|
||||
);
|
||||
},
|
||||
child: _buildListView(channelsBloc),
|
||||
);
|
||||
}
|
||||
|
||||
StreamBuilder<List<Channel>> _buildListView(
|
||||
ChannelsBlocState channelsBlocState,
|
||||
) {
|
||||
return StreamBuilder<List<Channel>>(
|
||||
stream: channelsBlocState.channelsStream,
|
||||
builder: (context, snapshot) {
|
||||
var child;
|
||||
if (snapshot.hasError) {
|
||||
child = _buildErrorWidget(
|
||||
snapshot,
|
||||
context,
|
||||
channelsBlocState,
|
||||
);
|
||||
} else if (!snapshot.hasData) {
|
||||
child = _buildLoadingWidget();
|
||||
} else {
|
||||
final channels = snapshot.data;
|
||||
|
||||
if (channels.isEmpty && widget.emptyBuilder != null) {
|
||||
child = widget.emptyBuilder(context);
|
||||
}
|
||||
|
||||
if (channels.isEmpty && widget.emptyBuilder == null) {
|
||||
child = LayoutBuilder(
|
||||
builder: (context, viewportConstraints) {
|
||||
return SingleChildScrollView(
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
child: Stack(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: viewportConstraints.maxHeight,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: StreamSvgIcon.message(
|
||||
size: 136,
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.greyGainsboro,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'Let’s start chatting!',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.headline,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
horizontal: 52,
|
||||
),
|
||||
child: Text(
|
||||
'How about sending your first message to a friend?',
|
||||
textAlign: TextAlign.center,
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.body
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.onStartChatPressed != null)
|
||||
Positioned(
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 32,
|
||||
child: Center(
|
||||
child: FlatButton(
|
||||
onPressed: widget.onStartChatPressed,
|
||||
child: Text(
|
||||
'Start a chat',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.bodyBold
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (channels.isNotEmpty) {
|
||||
if (widget.crossAxisCount > 1) {
|
||||
child = GridView.builder(
|
||||
padding: widget.padding,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: widget.crossAxisCount),
|
||||
itemCount: channels.length,
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
controller: _scrollController,
|
||||
itemBuilder: (context, index) {
|
||||
return _gridItemBuilder(context, index, channels);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
child = ListView.separated(
|
||||
padding: widget.padding,
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
itemCount:
|
||||
channels.isNotEmpty ? channels.length + 1 : channels.length,
|
||||
separatorBuilder: (_, index) {
|
||||
if (widget.separatorBuilder != null) {
|
||||
return widget.separatorBuilder(context, index);
|
||||
}
|
||||
return _separatorBuilder(context, index);
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
return _listItemBuilder(context, index, channels);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return AnimatedSwitcher(
|
||||
child: child,
|
||||
duration: Duration(milliseconds: 500),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingWidget() {
|
||||
return ListView(
|
||||
padding: widget.padding,
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
children: List.generate(
|
||||
25,
|
||||
(i) {
|
||||
if (widget.crossAxisCount == 1) {
|
||||
if (i % 2 != 0) {
|
||||
if (widget.separatorBuilder != null) {
|
||||
return widget.separatorBuilder(context, i);
|
||||
}
|
||||
return _separatorBuilder(context, i);
|
||||
}
|
||||
}
|
||||
return _buildLoadingItem();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Shimmer _buildLoadingItem() {
|
||||
if (widget.crossAxisCount > 1) {
|
||||
return Shimmer.fromColors(
|
||||
baseColor: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
highlightColor: StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 4.0),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
for (int i = 0; i < widget.crossAxisCount; i++)
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 70,
|
||||
width: 70,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
height: 16.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Shimmer.fromColors(
|
||||
baseColor: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
highlightColor: StreamChatTheme.of(context).colorTheme.whiteSmoke,
|
||||
child: ListTile(
|
||||
leading: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 40,
|
||||
width: 40,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
),
|
||||
title: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
borderRadius: BorderRadius.circular(11),
|
||||
),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 16,
|
||||
width: 82,
|
||||
),
|
||||
),
|
||||
),
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
borderRadius: BorderRadius.circular(11),
|
||||
),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 16,
|
||||
width: 238,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
borderRadius: BorderRadius.circular(11),
|
||||
),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 16,
|
||||
width: 42,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildErrorWidget(
|
||||
AsyncSnapshot<List<Channel>> snapshot,
|
||||
BuildContext context,
|
||||
ChannelsBlocState channelsBlocState,
|
||||
) {
|
||||
if (snapshot.error is Error) {
|
||||
print((snapshot.error as Error).stackTrace);
|
||||
}
|
||||
|
||||
if (widget.errorBuilder != null) {
|
||||
return widget.errorBuilder(snapshot.error);
|
||||
}
|
||||
|
||||
var message = snapshot.error.toString();
|
||||
if (snapshot.error is DioError) {
|
||||
final dioError = snapshot.error as DioError;
|
||||
if (dioError.type == DioErrorType.RESPONSE) {
|
||||
message = dioError.message;
|
||||
} else {
|
||||
message = 'Check your connection and retry';
|
||||
}
|
||||
}
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
WidgetSpan(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 2.0,
|
||||
),
|
||||
child: Icon(Icons.error_outline),
|
||||
),
|
||||
),
|
||||
TextSpan(text: 'Error loading channels'),
|
||||
],
|
||||
),
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 16.0,
|
||||
),
|
||||
child: Text(message),
|
||||
),
|
||||
FlatButton(
|
||||
onPressed: () {
|
||||
channelsBlocState.queryChannels(
|
||||
filter: widget.filter,
|
||||
sortOptions: widget.sort,
|
||||
paginationParams: widget.pagination,
|
||||
options: widget.options,
|
||||
);
|
||||
},
|
||||
child: Text('Retry'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _listItemBuilder(BuildContext context, int i, List<Channel> channels) {
|
||||
final channelsProvider = ChannelsBloc.of(context);
|
||||
if (i < channels.length) {
|
||||
final channel = channels[i];
|
||||
ChannelTapCallback onTap;
|
||||
if (widget.onChannelTap != null) {
|
||||
onTap = widget.onChannelTap;
|
||||
} else {
|
||||
onTap = (client, _) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return StreamChannel(
|
||||
child: widget.channelWidget,
|
||||
channel: client,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
return StreamChannel(
|
||||
key: ValueKey<String>('CHANNEL-${channel.id}'),
|
||||
channel: channel,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
Widget child;
|
||||
if (widget.channelPreviewBuilder != null) {
|
||||
child = Stack(
|
||||
children: [
|
||||
widget.channelPreviewBuilder(
|
||||
context,
|
||||
channel,
|
||||
),
|
||||
Positioned.fill(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
onTap(channel, widget.channelWidget);
|
||||
},
|
||||
onLongPress: widget.onChannelLongPress != null
|
||||
? () {
|
||||
widget.onChannelLongPress(channel);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
final backgroundColor =
|
||||
StreamChatTheme.of(context).colorTheme.whiteSmoke;
|
||||
child = Slidable(
|
||||
controller: _slideController,
|
||||
enabled: widget.swipeToAction,
|
||||
actionPane: SlidableBehindActionPane(),
|
||||
actionExtentRatio: 0.12,
|
||||
closeOnScroll: true,
|
||||
secondaryActions: <Widget>[
|
||||
IconSlideAction(
|
||||
color: backgroundColor,
|
||||
icon: Icons.more_horiz,
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(32),
|
||||
topRight: Radius.circular(32),
|
||||
),
|
||||
),
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return StreamChannel(
|
||||
child: ChannelBottomSheet(
|
||||
onViewInfoTap: () {
|
||||
widget.onViewInfoTap(channel);
|
||||
},
|
||||
),
|
||||
channel: channel,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
if ([
|
||||
'admin',
|
||||
'owner',
|
||||
].contains(channel.state.members
|
||||
.firstWhere(
|
||||
(m) => m.userId == channel.client.state.user.id,
|
||||
orElse: () => null)
|
||||
?.role))
|
||||
IconSlideAction(
|
||||
color: backgroundColor,
|
||||
iconWidget: StreamSvgIcon.delete(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentRed,
|
||||
),
|
||||
onTap: () async {
|
||||
final res = await showConfirmationDialog(
|
||||
context,
|
||||
title: 'Delete Conversation',
|
||||
okText: 'DELETE',
|
||||
question:
|
||||
'Are you sure you want to delete this conversation?',
|
||||
cancelText: 'CANCEL',
|
||||
icon: StreamSvgIcon.delete(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentRed,
|
||||
),
|
||||
);
|
||||
if (res == true) {
|
||||
await channel.delete();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
child: Container(
|
||||
color: StreamChatTheme.of(context).colorTheme.whiteSnow,
|
||||
child: ChannelPreview(
|
||||
onLongPress: widget.onChannelLongPress,
|
||||
channel: channel,
|
||||
onImageTap: widget.onImageTap != null
|
||||
? () {
|
||||
widget.onImageTap(channel);
|
||||
}
|
||||
: null,
|
||||
onTap: (channel) {
|
||||
onTap(channel, widget.channelWidget);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return child;
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return _buildQueryProgressIndicator(context, channelsProvider);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _gridItemBuilder(BuildContext context, int i, List<Channel> channels) {
|
||||
var channel = channels[i];
|
||||
|
||||
var selected = widget.selectedChannels.contains(channel);
|
||||
|
||||
return Container(
|
||||
key: ValueKey<String>('CHANNEL-${channel.id}'),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
ChannelImage(
|
||||
channel: channel,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
selected: selected,
|
||||
constraints: BoxConstraints.tightFor(
|
||||
width: 64,
|
||||
height: 64,
|
||||
),
|
||||
onTap: () {
|
||||
widget.onChannelTap(channel, null);
|
||||
},
|
||||
),
|
||||
SizedBox(height: 7),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: StreamChannel(
|
||||
child: ChannelName(
|
||||
textStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
channel: channel,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQueryProgressIndicator(
|
||||
context,
|
||||
ChannelsBlocState channelsProvider,
|
||||
) {
|
||||
return StreamBuilder<bool>(
|
||||
stream: channelsProvider.queryChannelsLoading,
|
||||
initialData: false,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Container(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentRed
|
||||
.withOpacity(.2),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: Center(
|
||||
child: Text('Error loading channels'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
height: 100,
|
||||
padding: EdgeInsets.all(32),
|
||||
child: Center(
|
||||
child: snapshot.data ? CircularProgressIndicator() : Container(),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _separatorBuilder(context, i) {
|
||||
var effect = StreamChatTheme.of(context).colorTheme.borderTop;
|
||||
|
||||
return BackdropFilter(
|
||||
filter: ui.ImageFilter.blur(
|
||||
sigmaX: effect.sigmaX,
|
||||
sigmaY: effect.sigmaY,
|
||||
),
|
||||
child: Container(
|
||||
height: 1,
|
||||
color: effect.color.withOpacity(0.08),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _listenChannelPagination(ChannelsBlocState channelsProvider) {
|
||||
if (_scrollController.position.maxScrollExtent ==
|
||||
_scrollController.offset &&
|
||||
_scrollController.offset != 0) {
|
||||
channelsProvider.queryChannels(
|
||||
filter: widget.filter,
|
||||
sortOptions: widget.sort,
|
||||
paginationParams: widget.pagination.copyWith(
|
||||
offset: channelsProvider.channels?.length ?? 0,
|
||||
),
|
||||
options: widget.options,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
StreamSubscription _subscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
|
||||
final channelsBloc = ChannelsBloc.of(context);
|
||||
channelsBloc.queryChannels(
|
||||
filter: widget.filter,
|
||||
sortOptions: widget.sort,
|
||||
paginationParams: widget.pagination,
|
||||
options: widget.options,
|
||||
);
|
||||
|
||||
_scrollController.addListener(() {
|
||||
channelsBloc.queryChannelsLoading.first.then((loading) {
|
||||
if (!loading) {
|
||||
_listenChannelPagination(channelsBloc);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
final client = StreamChat.of(context).client;
|
||||
|
||||
_subscription = client
|
||||
.on(
|
||||
EventType.connectionRecovered,
|
||||
EventType.notificationAddedToChannel,
|
||||
EventType.notificationMessageNew,
|
||||
EventType.channelVisible,
|
||||
)
|
||||
.listen((event) {
|
||||
channelsBloc.queryChannels(
|
||||
filter: widget.filter,
|
||||
sortOptions: widget.sort,
|
||||
paginationParams: widget.pagination,
|
||||
options: widget.options,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(ChannelListView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (widget.filter?.toString() != oldWidget.filter?.toString() ||
|
||||
jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) ||
|
||||
widget.pagination?.toJson()?.toString() !=
|
||||
oldWidget.pagination?.toJson()?.toString() ||
|
||||
widget.options?.toString() != oldWidget.options?.toString()) {
|
||||
final channelsBloc = ChannelsBloc.of(context);
|
||||
channelsBloc.queryChannels(
|
||||
filter: widget.filter,
|
||||
sortOptions: widget.sort,
|
||||
paginationParams: widget.pagination,
|
||||
options: widget.options,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription.cancel();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:video_player/video_player.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.
|
||||
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
|
||||
/// Direction can be ascending or descending.
|
||||
final List<SortOption> sortOptions;
|
||||
|
||||
/// Pagination parameters
|
||||
/// limit: the number of users to return (max is 30)
|
||||
/// offset: the offset (max is 1000)
|
||||
/// message_limit: how many messages should be included to each channel
|
||||
final PaginationParams paginationParams;
|
||||
|
||||
/// The builder used when the file list is empty.
|
||||
final WidgetBuilder emptyBuilder;
|
||||
|
||||
final ShowMessageCallback onShowMessage;
|
||||
|
||||
const ChannelMediaDisplayScreen({
|
||||
this.sortOptions,
|
||||
this.paginationParams,
|
||||
this.emptyBuilder,
|
||||
this.onShowMessage,
|
||||
});
|
||||
|
||||
@override
|
||||
_ChannelMediaDisplayScreenState createState() =>
|
||||
_ChannelMediaDisplayScreenState();
|
||||
}
|
||||
|
||||
class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final messageSearchBloc = MessageSearchBloc.of(context);
|
||||
messageSearchBloc.search(
|
||||
filter: {
|
||||
'cid': {
|
||||
r'$in': ['messaging:${StreamChannel.of(context).channel.id}']
|
||||
}
|
||||
},
|
||||
messageFilter: {
|
||||
'attachments.type': {
|
||||
r'$in': ['image', 'video']
|
||||
},
|
||||
},
|
||||
sort: widget.sortOptions,
|
||||
pagination: widget.paginationParams,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||
appBar: AppBar(
|
||||
brightness: Theme.of(context).brightness,
|
||||
elevation: 1,
|
||||
centerTitle: true,
|
||||
title: Text(
|
||||
'Photos & Videos',
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
fontSize: 16.0,
|
||||
),
|
||||
),
|
||||
leading: Center(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Container(
|
||||
child: StreamSvgIcon.left(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
size: 24.0,
|
||||
),
|
||||
width: 24.0,
|
||||
height: 24.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||
),
|
||||
body: _buildMediaGrid(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMediaGrid() {
|
||||
final messageSearchBloc = MessageSearchBloc.of(context);
|
||||
|
||||
return StreamBuilder<List<GetMessageResponse>>(
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.data == null) {
|
||||
return Center(
|
||||
child: const CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
if (snapshot.data.isEmpty) {
|
||||
if (widget.emptyBuilder != null) {
|
||||
return widget.emptyBuilder(context);
|
||||
}
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
StreamSvgIcon.pictures(
|
||||
size: 136.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
),
|
||||
SizedBox(height: 16.0),
|
||||
Text(
|
||||
'No Media',
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.0),
|
||||
Text(
|
||||
'Photos or video sent in this chat will \nappear here',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final media = <_AssetPackage>[];
|
||||
|
||||
for (var item in snapshot.data) {
|
||||
item.message.attachments
|
||||
.where((e) => e.type == 'image' || e.type == 'video')
|
||||
.forEach((e) {
|
||||
VideoPlayerController controller;
|
||||
if (e.type == 'video') {
|
||||
controller = VideoPlayerController.network(e.assetUrl);
|
||||
controller.initialize();
|
||||
}
|
||||
media.add(_AssetPackage(e, item.message, controller));
|
||||
});
|
||||
}
|
||||
|
||||
return LazyLoadScrollView(
|
||||
onEndOfPage: () => messageSearchBloc.search(
|
||||
filter: {
|
||||
'cid': {
|
||||
r'$in': ['messaging:${StreamChannel.of(context).channel.id}']
|
||||
}
|
||||
},
|
||||
messageFilter: {
|
||||
'attachments.type': {
|
||||
r'$in': ['image', 'video']
|
||||
},
|
||||
},
|
||||
sort: widget.sortOptions,
|
||||
pagination: widget.paginationParams.copyWith(
|
||||
offset: messageSearchBloc.messageResponses?.length ?? 0,
|
||||
),
|
||||
),
|
||||
child: GridView.builder(
|
||||
gridDelegate:
|
||||
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
|
||||
itemBuilder: (context, position) {
|
||||
var channel = StreamChannel.of(context).channel;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(1.0),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments:
|
||||
media.map((e) => e.attachment).toList(),
|
||||
startIndex: position,
|
||||
message: media[position].message,
|
||||
sentAt: media[position].message.createdAt,
|
||||
userName: media[position].message.user.name,
|
||||
onShowMessage: widget.onShowMessage,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: media[position].attachment.type == 'image'
|
||||
? IgnorePointer(
|
||||
child: ImageAttachment(
|
||||
attachment: media[position].attachment,
|
||||
message: media[position].message,
|
||||
showTitle: false,
|
||||
size: Size(
|
||||
MediaQuery.of(context).size.width * 0.8,
|
||||
MediaQuery.of(context).size.height * 0.3,
|
||||
),
|
||||
),
|
||||
)
|
||||
: VideoPlayer(media[position].videoPlayer),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: media.length,
|
||||
),
|
||||
);
|
||||
},
|
||||
stream: messageSearchBloc.messagesStream,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AssetPackage {
|
||||
Attachment attachment;
|
||||
Message message;
|
||||
VideoPlayerController videoPlayer;
|
||||
|
||||
_AssetPackage(this.attachment, this.message, this.videoPlayer);
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'stream_channel.dart';
|
||||
|
||||
/// It shows the current [Channel] name using a [Text] widget.
|
||||
///
|
||||
/// The widget uses a [StreamBuilder] to render the channel information image as soon as it updates.
|
||||
class ChannelName extends StatelessWidget {
|
||||
/// Instantiate a new ChannelName
|
||||
const ChannelName({
|
||||
Key key,
|
||||
this.textStyle,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The style of the text displayed
|
||||
final TextStyle textStyle;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final client = StreamChat.of(context);
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
|
||||
return StreamBuilder<Map<String, dynamic>>(
|
||||
stream: channel.extraDataStream,
|
||||
initialData: channel.extraData,
|
||||
builder: (context, snapshot) {
|
||||
return _buildName(snapshot.data, channel.state.members, client);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildName(
|
||||
Map<String, dynamic> extraData,
|
||||
List<Member> members,
|
||||
StreamChatState client,
|
||||
) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
String title;
|
||||
if (extraData['name'] == null) {
|
||||
final otherMembers =
|
||||
members.where((member) => member.userId != client.user.id);
|
||||
if (otherMembers.isNotEmpty) {
|
||||
final maxWidth = constraints.maxWidth;
|
||||
final maxChars = maxWidth / textStyle.fontSize;
|
||||
var currentChars = 0;
|
||||
final currentMembers = <Member>[];
|
||||
otherMembers.forEach((element) {
|
||||
final newLength = currentChars + element.user.name.length;
|
||||
if (newLength < maxChars) {
|
||||
currentChars = newLength;
|
||||
currentMembers.add(element);
|
||||
}
|
||||
});
|
||||
|
||||
final exceedingMembers =
|
||||
otherMembers.length - currentMembers.length;
|
||||
title =
|
||||
'${currentMembers.map((e) => e.user.name).join(', ')} ${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}';
|
||||
} else {
|
||||
title = 'No title';
|
||||
}
|
||||
} else {
|
||||
title = extraData['name'];
|
||||
}
|
||||
|
||||
return Text(
|
||||
title,
|
||||
style: textStyle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,298 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'channel_name.dart';
|
||||
import 'channel_unread_indicator.dart';
|
||||
|
||||
/// 
|
||||
/// 
|
||||
///
|
||||
/// It shows the current [Channel] preview.
|
||||
///
|
||||
/// The widget uses a [StreamBuilder] to render the channel information image as soon as it updates.
|
||||
///
|
||||
/// Usually you don't use this widget as it's the default channel preview used by [ChannelListView].
|
||||
///
|
||||
/// The widget renders the ui based on the first ancestor of type [StreamChatTheme].
|
||||
/// Modify it to change the widget appearance.
|
||||
class ChannelPreview extends StatelessWidget {
|
||||
/// Function called when tapping this widget
|
||||
final void Function(Channel) onTap;
|
||||
|
||||
/// Function called when long pressing this widget
|
||||
final void Function(Channel) onLongPress;
|
||||
|
||||
/// Channel displayed
|
||||
final Channel channel;
|
||||
|
||||
/// The function called when the image is tapped
|
||||
final VoidCallback onImageTap;
|
||||
|
||||
ChannelPreview({
|
||||
@required this.channel,
|
||||
Key key,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.onImageTap,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder<bool>(
|
||||
stream: channel.isMutedStream,
|
||||
initialData: channel.isMuted,
|
||||
builder: (context, snapshot) {
|
||||
return Opacity(
|
||||
opacity: snapshot.data ? 0.5 : 1,
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
),
|
||||
onTap: () {
|
||||
if (onTap != null) {
|
||||
onTap(channel);
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
if (onLongPress != null) {
|
||||
onLongPress(channel);
|
||||
}
|
||||
},
|
||||
leading: ChannelImage(
|
||||
onTap: onImageTap,
|
||||
),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: ChannelName(
|
||||
textStyle:
|
||||
StreamChatTheme.of(context).channelPreviewTheme.title,
|
||||
),
|
||||
),
|
||||
StreamBuilder<List<Member>>(
|
||||
stream: channel.state.membersStream,
|
||||
initialData: channel.state.members,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData ||
|
||||
snapshot.data.isEmpty ||
|
||||
!snapshot.data.any((Member e) =>
|
||||
e.user.id == channel.client.state.user.id)) {
|
||||
return SizedBox();
|
||||
}
|
||||
return ChannelUnreadIndicator(
|
||||
channel: channel,
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Flexible(child: _buildSubtitle(context)),
|
||||
Builder(
|
||||
builder: (context) {
|
||||
if (channel.state.lastMessage?.user?.id ==
|
||||
StreamChat.of(context).user.id) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 4.0),
|
||||
child: SendingIndicator(
|
||||
message: channel.state.lastMessage,
|
||||
size: StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.indicatorIconSize,
|
||||
isMessageRead: channel.state.read
|
||||
?.where((element) =>
|
||||
element.user.id !=
|
||||
channel.client.state.user.id)
|
||||
?.where((element) => element.lastRead
|
||||
.isAfter(channel
|
||||
.state.lastMessage.createdAt))
|
||||
?.isNotEmpty ==
|
||||
true,
|
||||
),
|
||||
);
|
||||
}
|
||||
return SizedBox();
|
||||
},
|
||||
),
|
||||
_buildDate(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildDate(BuildContext context) {
|
||||
return StreamBuilder<DateTime>(
|
||||
stream: channel.lastMessageAtStream,
|
||||
initialData: channel.lastMessageAt,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return SizedBox();
|
||||
}
|
||||
final lastMessageAt = snapshot.data.toLocal();
|
||||
|
||||
String stringDate;
|
||||
final now = DateTime.now();
|
||||
|
||||
var startOfDay = DateTime(now.year, now.month, now.day);
|
||||
|
||||
if (lastMessageAt.millisecondsSinceEpoch >=
|
||||
startOfDay.millisecondsSinceEpoch) {
|
||||
stringDate = Jiffy(lastMessageAt.toLocal()).format('HH:mm');
|
||||
} else if (lastMessageAt.millisecondsSinceEpoch >=
|
||||
startOfDay.subtract(Duration(days: 1)).millisecondsSinceEpoch) {
|
||||
stringDate = 'Yesterday';
|
||||
} else if (startOfDay.difference(lastMessageAt).inDays < 7) {
|
||||
stringDate = Jiffy(lastMessageAt.toLocal()).EEEE;
|
||||
} else {
|
||||
stringDate = Jiffy(lastMessageAt.toLocal()).format('dd/MM/yyyy');
|
||||
}
|
||||
|
||||
return Text(
|
||||
stringDate,
|
||||
style: StreamChatTheme.of(context).channelPreviewTheme.lastMessageAt,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubtitle(BuildContext context) {
|
||||
if (channel.isMuted) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
StreamSvgIcon.mute(
|
||||
size: 16,
|
||||
),
|
||||
Text(
|
||||
' Channel is muted',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.subtitle
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.subtitle
|
||||
.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return TypingIndicator(
|
||||
channel: channel,
|
||||
alternativeWidget: _buildLastMessage(context),
|
||||
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
|
||||
color:
|
||||
StreamChatTheme.of(context).channelPreviewTheme.subtitle.color,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLastMessage(BuildContext context) {
|
||||
return StreamBuilder<List<Message>>(
|
||||
stream: channel.state.messagesStream,
|
||||
initialData: channel.state.messages,
|
||||
builder: (context, snapshot) {
|
||||
final lastMessage = snapshot.data
|
||||
?.lastWhere((m) => m.shadowed != true, orElse: () => null);
|
||||
if (lastMessage == null) {
|
||||
return SizedBox();
|
||||
}
|
||||
|
||||
var text = lastMessage.text;
|
||||
if (lastMessage.isDeleted) {
|
||||
text = 'This message was deleted.';
|
||||
} else if (lastMessage.attachments != null) {
|
||||
final parts = <String>[
|
||||
...lastMessage.attachments.map((e) {
|
||||
if (e.type == 'image') {
|
||||
return '📷';
|
||||
} else if (e.type == 'video') {
|
||||
return '🎬';
|
||||
} else if (e.type == 'giphy') {
|
||||
return '[GIF]';
|
||||
}
|
||||
return e == lastMessage.attachments.last
|
||||
? (e.title ?? 'File')
|
||||
: '${e.title ?? 'File'} , ';
|
||||
}).where((e) => e != null),
|
||||
lastMessage.text ?? '',
|
||||
];
|
||||
|
||||
text = parts.join(' ');
|
||||
}
|
||||
|
||||
return Text.rich(
|
||||
_getDisplayText(
|
||||
text,
|
||||
lastMessage.mentionedUsers,
|
||||
lastMessage.attachments,
|
||||
StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.subtitle
|
||||
.color,
|
||||
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted)
|
||||
? FontStyle.italic
|
||||
: FontStyle.normal),
|
||||
StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.subtitle
|
||||
.color,
|
||||
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted)
|
||||
? FontStyle.italic
|
||||
: FontStyle.normal,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
TextSpan _getDisplayText(
|
||||
String text,
|
||||
List<User> mentions,
|
||||
List<Attachment> attachments,
|
||||
TextStyle normalTextStyle,
|
||||
TextStyle mentionsTextStyle) {
|
||||
var textList = text.split(' ');
|
||||
List<TextSpan> resList = [];
|
||||
for (var e in textList) {
|
||||
if (mentions != null &&
|
||||
mentions.isNotEmpty &&
|
||||
mentions.any((element) => '@${element.name}' == e)) {
|
||||
resList.add(TextSpan(
|
||||
text: '$e ',
|
||||
style: mentionsTextStyle,
|
||||
));
|
||||
} else if (attachments != null &&
|
||||
attachments.isNotEmpty &&
|
||||
attachments
|
||||
.where((e) => e.title != null)
|
||||
.any((element) => element.title == e)) {
|
||||
resList.add(TextSpan(
|
||||
text: '$e ',
|
||||
style: normalTextStyle.copyWith(fontStyle: FontStyle.italic),
|
||||
));
|
||||
} else {
|
||||
resList.add(TextSpan(
|
||||
text: e == textList.last ? '$e' : '$e ',
|
||||
style: normalTextStyle,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return TextSpan(children: resList);
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
|
||||
class ChannelUnreadIndicator extends StatelessWidget {
|
||||
const ChannelUnreadIndicator({
|
||||
Key key,
|
||||
@required this.channel,
|
||||
}) : super(key: key);
|
||||
|
||||
final Channel channel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder<int>(
|
||||
stream: channel.state.unreadCountStream,
|
||||
initialData: channel.state.unreadCount,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData || snapshot.data == 0) {
|
||||
return SizedBox();
|
||||
}
|
||||
|
||||
return Material(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.unreadCounterColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 5.0,
|
||||
right: 5.0,
|
||||
top: 2,
|
||||
bottom: 1,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${snapshot.data > 99 ? '99+' : snapshot.data}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// Widget dedicated to the management of a channel list with pagination
|
||||
class ChannelsBloc extends StatefulWidget {
|
||||
/// The widget child
|
||||
final Widget child;
|
||||
|
||||
/// Set this to true to prevent channels to be brought to the top of the list when a new message arrives
|
||||
final bool lockChannelsOrder;
|
||||
|
||||
/// Comparator used to sort the channels when a message.new event is received
|
||||
final Comparator<Channel> channelsComparator;
|
||||
|
||||
/// Function used to evaluate if a channel should be added to the list when a message.new event is received
|
||||
final bool Function(Event) shouldAddChannel;
|
||||
|
||||
/// Instantiate a new ChannelsBloc
|
||||
const ChannelsBloc({
|
||||
Key key,
|
||||
this.child,
|
||||
this.lockChannelsOrder = false,
|
||||
this.channelsComparator,
|
||||
this.shouldAddChannel,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
ChannelsBlocState createState() => ChannelsBlocState();
|
||||
|
||||
/// Use this method to get the current [ChannelsBlocState] instance
|
||||
static ChannelsBlocState of(BuildContext context) {
|
||||
ChannelsBlocState streamChatState;
|
||||
|
||||
streamChatState = context.findAncestorStateOfType<ChannelsBlocState>();
|
||||
|
||||
if (streamChatState == null) {
|
||||
throw Exception('You must have a ChannelsBloc widget as ancestor');
|
||||
}
|
||||
|
||||
return streamChatState;
|
||||
}
|
||||
}
|
||||
|
||||
/// The current state of the [ChannelsBloc]
|
||||
class ChannelsBlocState extends State<ChannelsBloc>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return widget.child;
|
||||
}
|
||||
|
||||
/// The current channel list
|
||||
List<Channel> get channels => _channelsController.value;
|
||||
|
||||
/// The current channel list as a stream
|
||||
Stream<List<Channel>> get channelsStream => _channelsController.stream;
|
||||
|
||||
final BehaviorSubject<bool> _queryChannelsLoadingController =
|
||||
BehaviorSubject.seeded(false);
|
||||
|
||||
final BehaviorSubject<List<Channel>> _channelsController = BehaviorSubject();
|
||||
|
||||
/// The stream notifying the state of queryChannel call
|
||||
Stream<bool> get queryChannelsLoading =>
|
||||
_queryChannelsLoadingController.stream;
|
||||
|
||||
final List<Channel> _hiddenChannels = [];
|
||||
|
||||
/// Calls [client.queryChannels] updating [queryChannelsLoading] stream
|
||||
Future<void> queryChannels({
|
||||
Map<String, dynamic> filter,
|
||||
List<SortOption> sortOptions,
|
||||
PaginationParams paginationParams,
|
||||
Map<String, dynamic> options,
|
||||
bool onlyOffline = false,
|
||||
}) async {
|
||||
final client = StreamChat.of(context).client;
|
||||
|
||||
if (client.state?.user == null ||
|
||||
_queryChannelsLoadingController.value == true) {
|
||||
return;
|
||||
}
|
||||
_queryChannelsLoadingController.sink.add(true);
|
||||
|
||||
try {
|
||||
final clear = paginationParams == null ||
|
||||
paginationParams.offset == null ||
|
||||
paginationParams.offset == 0;
|
||||
final oldChannels = List<Channel>.from(channels ?? []);
|
||||
final _channels = await client.queryChannels(
|
||||
filter: filter,
|
||||
sort: sortOptions,
|
||||
options: options,
|
||||
paginationParams: paginationParams,
|
||||
onlyOffline: onlyOffline,
|
||||
);
|
||||
|
||||
if (clear) {
|
||||
_channelsController.add(_channels);
|
||||
} else {
|
||||
final l = oldChannels + _channels;
|
||||
_channelsController.add(l);
|
||||
}
|
||||
_queryChannelsLoadingController.sink.add(false);
|
||||
} catch (err, stackTrace) {
|
||||
print(err);
|
||||
print(stackTrace);
|
||||
_queryChannelsLoadingController.addError(err, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
final client = StreamChat.of(context).client;
|
||||
|
||||
if (!widget.lockChannelsOrder) {
|
||||
_subscriptions.add(client.on(EventType.messageNew).listen((e) {
|
||||
final newChannels = List<Channel>.from(channels ?? []);
|
||||
final index = newChannels.indexWhere((c) => c.cid == e.cid);
|
||||
if (index > -1) {
|
||||
if (index > 0) {
|
||||
final channel = newChannels.removeAt(index);
|
||||
newChannels.insert(0, channel);
|
||||
}
|
||||
} else if (widget.shouldAddChannel != null &&
|
||||
widget.shouldAddChannel(e)) {
|
||||
final hiddenIndex = _hiddenChannels.indexWhere((c) => c.cid == e.cid);
|
||||
if (hiddenIndex > -1) {
|
||||
newChannels.insert(0, _hiddenChannels[hiddenIndex]);
|
||||
_hiddenChannels.removeAt(hiddenIndex);
|
||||
} else {
|
||||
if (client.state?.channels != null &&
|
||||
client.state?.channels[e.cid] != null) {
|
||||
newChannels.insert(0, client.state.channels[e.cid]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (widget.channelsComparator != null) {
|
||||
newChannels.sort(widget.channelsComparator);
|
||||
}
|
||||
_channelsController.add(newChannels);
|
||||
}));
|
||||
}
|
||||
|
||||
_subscriptions.add(client.on(EventType.channelHidden).listen((event) async {
|
||||
final newChannels = List<Channel>.from(channels ?? []);
|
||||
final channelIndex = newChannels.indexWhere((c) => c.cid == event.cid);
|
||||
if (channelIndex > -1) {
|
||||
final channel = newChannels.removeAt(channelIndex);
|
||||
_hiddenChannels.add(channel);
|
||||
_channelsController.add(newChannels);
|
||||
}
|
||||
}));
|
||||
|
||||
_subscriptions.add(client
|
||||
.on(EventType.channelDeleted, EventType.notificationRemovedFromChannel)
|
||||
.listen((e) {
|
||||
final channel = e.channel;
|
||||
_channelsController
|
||||
.add(List.from(channels..removeWhere((c) => c.cid == channel.cid)));
|
||||
}));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_channelsController.close();
|
||||
_queryChannelsLoadingController.close();
|
||||
_subscriptions.forEach((s) => s.cancel());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
@@ -1,20 +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> compressVideo(String path) async {
|
||||
return _lock.synchronized(() {
|
||||
return VideoCompress.compressVideo(
|
||||
path,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ICompressVideoService get CompressVideoService =>
|
||||
ICompressVideoService.instance;
|
||||
@@ -1,58 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
|
||||
/// It shows a date divider depending on the date difference
|
||||
class DateDivider extends StatelessWidget {
|
||||
final DateTime dateTime;
|
||||
|
||||
const DateDivider({
|
||||
Key key,
|
||||
@required this.dateTime,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final createdAt = Jiffy(dateTime);
|
||||
final now = DateTime.now();
|
||||
|
||||
String dayInfo;
|
||||
if (Jiffy(createdAt).isSame(now, Units.DAY)) {
|
||||
dayInfo = 'TODAY';
|
||||
} else if (Jiffy(createdAt)
|
||||
.isSame(now.subtract(Duration(days: 1)), Units.DAY)) {
|
||||
dayInfo = 'YESTERDAY';
|
||||
} else if (Jiffy(createdAt).isAfter(
|
||||
now.subtract(Duration(days: 7)),
|
||||
Units.DAY,
|
||||
)) {
|
||||
dayInfo = createdAt.format('EEEE').toUpperCase();
|
||||
} else if (Jiffy(createdAt).isAfter(
|
||||
Jiffy(now).subtract(years: 1),
|
||||
Units.DAY,
|
||||
)) {
|
||||
dayInfo = createdAt.format('MMMM d').toUpperCase();
|
||||
} else {
|
||||
dayInfo = createdAt.format('MMMM d').toUpperCase();
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(
|
||||
8,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
dayInfo,
|
||||
style: StreamChatTheme.of(context).textTheme.footnoteBold.copyWith(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
|
||||
class DeletedMessage extends StatelessWidget {
|
||||
const DeletedMessage({
|
||||
Key key,
|
||||
@required this.messageTheme,
|
||||
this.borderRadiusGeometry,
|
||||
this.shape,
|
||||
this.borderSide,
|
||||
this.reverse = false,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The theme of the message
|
||||
final MessageTheme messageTheme;
|
||||
|
||||
/// The border radius of the message text
|
||||
final BorderRadiusGeometry borderRadiusGeometry;
|
||||
|
||||
/// The shape of the message text
|
||||
final ShapeBorder shape;
|
||||
|
||||
/// The borderside of the message text
|
||||
final BorderSide borderSide;
|
||||
|
||||
/// If true the widget will be mirrored
|
||||
final bool reverse;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Transform(
|
||||
transform: Matrix4.rotationY(reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: Material(
|
||||
color: messageTheme.messageBackgroundColor,
|
||||
shape: shape ??
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: borderRadiusGeometry ?? BorderRadius.zero,
|
||||
side: borderSide ??
|
||||
BorderSide(
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.white
|
||||
.withAlpha(24)
|
||||
: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withAlpha(24),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: Transform(
|
||||
transform: Matrix4.rotationY(reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'Message deleted',
|
||||
style: messageTheme.messageText.copyWith(
|
||||
fontStyle: FontStyle.italic,
|
||||
color: (Theme.of(context).brightness == Brightness.dark
|
||||
? StreamChatTheme.of(context).colorTheme.white
|
||||
: StreamChatTheme.of(context).colorTheme.black)
|
||||
.withOpacity(.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
extension StringExtension on String {
|
||||
String capitalize() {
|
||||
return "${this[0].toUpperCase()}${this.substring(1)}";
|
||||
}
|
||||
}
|
||||
|
||||
/// List extension
|
||||
extension IterableX<T> on Iterable<T> {
|
||||
/// Insert any item<T> inBetween the list items
|
||||
List<T> insertBetween(T item) => expand((e) sync* {
|
||||
yield item;
|
||||
yield e;
|
||||
}).skip(1).toList(growable: false);
|
||||
}
|
||||
@@ -1,200 +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/stream_chat.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: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:
|
||||
widget.trailing != null ? BorderRadius.circular(16.0) : null,
|
||||
border: widget.trailing != null
|
||||
? Border.fromBorderSide(BorderSide(
|
||||
color: StreamChatTheme.of(context).colorTheme.greyWhisper))
|
||||
: null,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
child: _getFileTypeImage(),
|
||||
height: 40.0,
|
||||
width: 33.33,
|
||||
margin: EdgeInsets.all(8.0),
|
||||
),
|
||||
SizedBox(
|
||||
width: 6.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(
|
||||
'${getSizeText(widget.attachment.extraData['file_size'])}',
|
||||
style: StreamChatTheme.of(context).textTheme.body.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
widget.trailing ??
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.cloud_download(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
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,
|
||||
);
|
||||
break;
|
||||
case FileAttachmentType.online:
|
||||
return CachedNetworkImage(
|
||||
imageUrl: widget.attachment.imageUrl ??
|
||||
widget.attachment.assetUrl ??
|
||||
widget.attachment.thumbUrl,
|
||||
fit: BoxFit.cover,
|
||||
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,268 +0,0 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:photo_view/photo_view.dart';
|
||||
import 'package:stream_chat_flutter/src/image_footer.dart';
|
||||
import 'package:stream_chat_flutter/src/image_header.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'stream_channel.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
|
||||
typedef ShowMessageCallback = void Function(Message message, Channel channel);
|
||||
|
||||
/// A full screen image widget
|
||||
class FullScreenMedia extends StatefulWidget {
|
||||
/// The url of the image
|
||||
final List<Attachment> mediaAttachments;
|
||||
final Message message;
|
||||
|
||||
final int startIndex;
|
||||
final String userName;
|
||||
final DateTime sentAt;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
|
||||
/// Instantiate a new FullScreenImage
|
||||
const FullScreenMedia({
|
||||
Key key,
|
||||
@required this.mediaAttachments,
|
||||
this.message,
|
||||
this.startIndex = 0,
|
||||
this.userName = '',
|
||||
this.sentAt,
|
||||
this.onShowMessage,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_FullScreenMediaState createState() => _FullScreenMediaState();
|
||||
}
|
||||
|
||||
class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
with SingleTickerProviderStateMixin {
|
||||
bool _optionsShown = true;
|
||||
|
||||
AnimationController _controller;
|
||||
PageController _pageController;
|
||||
|
||||
int _currentPage;
|
||||
|
||||
List<VideoPackage> videoPackages = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_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(() {});
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var videoAttachments = widget.mediaAttachments
|
||||
.where((element) => element.type == 'video')
|
||||
.toList();
|
||||
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: Stack(
|
||||
children: [
|
||||
AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, snapshot) {
|
||||
return PageView.builder(
|
||||
controller: _pageController,
|
||||
onPageChanged: (val) {
|
||||
setState(() {
|
||||
_currentPage = val;
|
||||
});
|
||||
},
|
||||
itemBuilder: (context, position) {
|
||||
if (widget.mediaAttachments[position].type == 'image' ||
|
||||
widget.mediaAttachments[position].type == 'giphy') {
|
||||
return PhotoView(
|
||||
imageProvider: CachedNetworkImageProvider(
|
||||
widget.mediaAttachments[position].imageUrl ??
|
||||
widget.mediaAttachments[position].assetUrl ??
|
||||
widget.mediaAttachments[position].thumbUrl),
|
||||
maxScale: PhotoViewComputedScale.covered,
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
heroAttributes: PhotoViewHeroAttributes(
|
||||
tag: widget.mediaAttachments,
|
||||
),
|
||||
backgroundDecoration: BoxDecoration(
|
||||
color: ColorTween(
|
||||
begin: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.color,
|
||||
end: Colors.black)
|
||||
.lerp(_controller.value),
|
||||
),
|
||||
onTapUp: (a, b, c) {
|
||||
setState(() {
|
||||
_optionsShown = !_optionsShown;
|
||||
});
|
||||
if (_controller.isCompleted) {
|
||||
_controller.reverse();
|
||||
} else {
|
||||
_controller.forward();
|
||||
}
|
||||
},
|
||||
);
|
||||
} else if (widget.mediaAttachments[position].type ==
|
||||
'video') {
|
||||
var controllerPackage = videoPackages[videoAttachments
|
||||
.indexOf(widget.mediaAttachments[position])];
|
||||
|
||||
if (!controllerPackage.initialised) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_optionsShown = !_optionsShown;
|
||||
});
|
||||
if (_controller.isCompleted) {
|
||||
_controller.reverse();
|
||||
} else {
|
||||
_controller.forward();
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 50.0,
|
||||
),
|
||||
child: Chewie(
|
||||
controller: controllerPackage.chewieController,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
itemCount: widget.mediaAttachments.length,
|
||||
);
|
||||
}),
|
||||
AnimatedOpacity(
|
||||
opacity: _optionsShown ? 1.0 : 0.0,
|
||||
duration: Duration(milliseconds: 300),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ImageHeader(
|
||||
userName: widget.userName,
|
||||
sentAt: widget.message.createdAt == null
|
||||
? ''
|
||||
: 'Sent ${getDay(widget.message.createdAt)} at ${Jiffy(widget.sentAt.toLocal()).format('HH:mm')}',
|
||||
onBackPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
message: widget.message,
|
||||
urls: widget.mediaAttachments,
|
||||
currentIndex: _currentPage,
|
||||
onShowMessage: () {
|
||||
widget.onShowMessage(
|
||||
widget.message, StreamChannel.of(context).channel);
|
||||
},
|
||||
),
|
||||
ImageFooter(
|
||||
currentPage: _currentPage,
|
||||
totalPages: widget.mediaAttachments.length,
|
||||
mediaAttachments: widget.mediaAttachments,
|
||||
message: widget.message,
|
||||
videoPackages: videoPackages,
|
||||
mediaSelectedCallBack: (val) {
|
||||
setState(() {
|
||||
_currentPage = val;
|
||||
_pageController.animateToPage(val,
|
||||
duration: Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut);
|
||||
Navigator.pop(context);
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String getDay(DateTime dateTime) {
|
||||
var now = DateTime.now();
|
||||
|
||||
if (DateTime(dateTime.year, dateTime.month, dateTime.day) ==
|
||||
DateTime(now.year, now.month, now.day)) {
|
||||
return 'today';
|
||||
} else if (DateTime(now.year, now.month, now.day)
|
||||
.difference(dateTime)
|
||||
.inHours <
|
||||
24) {
|
||||
return 'yesterday';
|
||||
} else {
|
||||
return 'on ${Jiffy(dateTime).format("MMM do")}';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
videoPackages.forEach((element) {
|
||||
element.dispose();
|
||||
});
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class VideoPackage {
|
||||
VideoPlayerController _videoPlayerController;
|
||||
ChewieController _chewieController;
|
||||
bool initialised = false;
|
||||
VoidCallback onInit;
|
||||
BuildContext context;
|
||||
|
||||
///
|
||||
VideoPackage(this.context, Attachment attachment, this.onInit) {
|
||||
_videoPlayerController = VideoPlayerController.network(attachment.assetUrl);
|
||||
_videoPlayerController.initialize().whenComplete(() {
|
||||
initialised = true;
|
||||
_chewieController = ChewieController(
|
||||
videoPlayerController: _videoPlayerController,
|
||||
autoInitialize: false,
|
||||
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);
|
||||
}
|
||||
|
||||
get videoPlayer => _videoPlayerController;
|
||||
|
||||
get chewieController => _chewieController;
|
||||
|
||||
void dispose() {
|
||||
_videoPlayerController.dispose();
|
||||
_chewieController.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,401 +0,0 @@
|
||||
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 '../stream_chat_flutter.dart';
|
||||
import 'attachment_error.dart';
|
||||
import 'full_screen_media.dart';
|
||||
|
||||
class GiphyAttachment extends StatelessWidget {
|
||||
final Attachment attachment;
|
||||
final MessageTheme messageTheme;
|
||||
final Message message;
|
||||
final Size size;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
|
||||
const GiphyAttachment({
|
||||
Key key,
|
||||
this.attachment,
|
||||
this.messageTheme,
|
||||
this.message,
|
||||
this.size,
|
||||
this.onShowMessage,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (attachment.thumbUrl == null &&
|
||||
attachment.imageUrl == null &&
|
||||
attachment.assetUrl == null) {
|
||||
return AttachmentError(
|
||||
attachment: attachment,
|
||||
);
|
||||
}
|
||||
|
||||
return attachment.actions != null
|
||||
? _buildSendingAttachment(context)
|
||||
: _buildSentAttachment(context);
|
||||
}
|
||||
|
||||
Widget _buildSendingAttachment(context) {
|
||||
final streamChannel = StreamChannel.of(context);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Card(
|
||||
elevation: 2,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(16.0),
|
||||
bottomRight: Radius.circular(0.0),
|
||||
topLeft: Radius.circular(16.0),
|
||||
bottomLeft: Radius.circular(16.0),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
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,
|
||||
),
|
||||
);
|
||||
}));
|
||||
},
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(8),
|
||||
topRight: Radius.circular(8),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
left: 16,
|
||||
child: Material(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 4.0,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
StreamSvgIcon.lightning(
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.white,
|
||||
size: 16,
|
||||
),
|
||||
Text(
|
||||
'GIPHY',
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (attachment.title != null)
|
||||
Container(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Card(
|
||||
elevation: 2,
|
||||
child: IconButton(
|
||||
padding: const EdgeInsets.all(0),
|
||||
constraints: BoxConstraints.tight(Size(32, 32)),
|
||||
icon: StreamSvgIcon.left(
|
||||
size: 24.0,
|
||||
),
|
||||
splashRadius: 16,
|
||||
onPressed: () {
|
||||
streamChannel.channel.sendAction(message, {
|
||||
'image_action': 'shuffle',
|
||||
});
|
||||
},
|
||||
),
|
||||
shape: CircleBorder(),
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
'"${attachment.title}"',
|
||||
style: TextStyle(
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Card(
|
||||
elevation: 2,
|
||||
child: IconButton(
|
||||
padding: const EdgeInsets.all(0),
|
||||
constraints: BoxConstraints.tight(Size(32, 32)),
|
||||
icon: StreamSvgIcon.right(
|
||||
size: 24.0,
|
||||
),
|
||||
splashRadius: 16,
|
||||
onPressed: () {
|
||||
streamChannel.channel.sendAction(message, {
|
||||
'image_action': 'shuffle',
|
||||
});
|
||||
},
|
||||
),
|
||||
shape: CircleBorder(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 4.0,
|
||||
),
|
||||
Container(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.2),
|
||||
width: double.infinity,
|
||||
height: 0.5,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlatButton(
|
||||
height: 50,
|
||||
onPressed: () {
|
||||
streamChannel.channel.sendAction(message, {
|
||||
'image_action': 'cancel',
|
||||
});
|
||||
},
|
||||
child: Text(
|
||||
'Cancel',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.bodyBold
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 0.5,
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.2),
|
||||
height: 50.0,
|
||||
),
|
||||
Expanded(
|
||||
child: FlatButton(
|
||||
height: 50,
|
||||
onPressed: () {
|
||||
streamChannel.channel.sendAction(message, {
|
||||
'image_action': 'send',
|
||||
});
|
||||
},
|
||||
child: Text(
|
||||
'Send',
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue,
|
||||
fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 4.0,
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
StreamSvgIcon.eye(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5),
|
||||
size: 16.0,
|
||||
),
|
||||
SizedBox(
|
||||
width: 8.0,
|
||||
),
|
||||
Text(
|
||||
'Only visible to you',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.footnote
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSentAttachment(context) {
|
||||
return Container(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (_) {
|
||||
var channel = StreamChannel.of(context).channel;
|
||||
|
||||
return StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments: [
|
||||
attachment,
|
||||
],
|
||||
userName: message.user.name,
|
||||
sentAt: message.createdAt,
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
),
|
||||
);
|
||||
}));
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
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,
|
||||
),
|
||||
Positioned(
|
||||
bottom: 8,
|
||||
left: 8,
|
||||
child: Material(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
vertical: 4.0,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
StreamSvgIcon.lightning(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
size: 16,
|
||||
),
|
||||
Text(
|
||||
'GIPHY',
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
|
||||
class GroupImage extends StatelessWidget {
|
||||
const GroupImage({
|
||||
Key key,
|
||||
@required this.images,
|
||||
this.constraints,
|
||||
this.onTap,
|
||||
this.borderRadius,
|
||||
this.selected = false,
|
||||
this.selectionColor,
|
||||
this.selectionThickness = 4,
|
||||
}) : super(key: key);
|
||||
|
||||
final List<String> images;
|
||||
final BoxConstraints constraints;
|
||||
final VoidCallback onTap;
|
||||
final bool selected;
|
||||
final BorderRadius borderRadius;
|
||||
final Color selectionColor;
|
||||
final double selectionThickness;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var avatar;
|
||||
final streamChatTheme = StreamChatTheme.of(context);
|
||||
|
||||
avatar = GestureDetector(
|
||||
onTap: onTap,
|
||||
child: ClipRRect(
|
||||
borderRadius: borderRadius ??
|
||||
StreamChatTheme.of(context)
|
||||
.ownMessageTheme
|
||||
.avatarTheme
|
||||
.borderRadius,
|
||||
child: Container(
|
||||
constraints: constraints ??
|
||||
StreamChatTheme.of(context)
|
||||
.ownMessageTheme
|
||||
.avatarTheme
|
||||
.constraints,
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
),
|
||||
child: Flex(
|
||||
direction: Axis.vertical,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Flexible(
|
||||
fit: FlexFit.tight,
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: images
|
||||
.take(2)
|
||||
.map((url) => Flexible(
|
||||
fit: FlexFit.tight,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.cover,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Transform.scale(
|
||||
scale: 1.2,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: url,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
if (images.length > 2)
|
||||
Flexible(
|
||||
fit: FlexFit.tight,
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: images
|
||||
.skip(2)
|
||||
.map((url) => Flexible(
|
||||
fit: FlexFit.tight,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.cover,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Transform.scale(
|
||||
scale: 1.2,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: url,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (selected) {
|
||||
avatar = ClipRRect(
|
||||
borderRadius: (borderRadius ??
|
||||
streamChatTheme.ownMessageTheme.avatarTheme.borderRadius) +
|
||||
BorderRadius.circular(selectionThickness),
|
||||
child: Container(
|
||||
color: selectionColor ??
|
||||
StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
height: 64.0,
|
||||
width: 64.0,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(selectionThickness),
|
||||
child: avatar,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return avatar;
|
||||
}
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_gallery_saver/image_gallery_saver.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
|
||||
class ImageActionsModal extends StatelessWidget {
|
||||
final Message message;
|
||||
final String userName;
|
||||
final String sentAt;
|
||||
final List<Attachment> urls;
|
||||
final currentIndex;
|
||||
final VoidCallback onShowMessage;
|
||||
|
||||
ImageActionsModal(
|
||||
{this.message,
|
||||
this.userName,
|
||||
this.sentAt,
|
||||
this.urls,
|
||||
this.currentIndex,
|
||||
this.onShowMessage});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => Navigator.maybePop(context),
|
||||
child: _buildPage(context),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPage(context) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: kToolbarHeight,
|
||||
child: IconButton(
|
||||
icon: StreamSvgIcon.close(),
|
||||
onPressed: () => Navigator.maybePop(context),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Container(
|
||||
width: MediaQuery.of(context).size.width / 1.8,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Material(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: ListTile.divideTiles(
|
||||
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
|
||||
context: context,
|
||||
tiles: [
|
||||
_buildButton(
|
||||
context,
|
||||
'Reply',
|
||||
StreamSvgIcon.Icon_curve_line_left_up(
|
||||
size: 24.0,
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
() {}),
|
||||
_buildButton(
|
||||
context,
|
||||
'Show in Chat',
|
||||
StreamSvgIcon.eye(
|
||||
size: 24.0,
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
onShowMessage),
|
||||
_buildButton(
|
||||
context,
|
||||
'Save ${urls[currentIndex].type == 'video' ? 'Video' : 'Image'}',
|
||||
StreamSvgIcon.Icon_save(
|
||||
size: 24.0,
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.grey,
|
||||
), () async {
|
||||
var url = urls[currentIndex].imageUrl ??
|
||||
urls[currentIndex].assetUrl ??
|
||||
urls[currentIndex].thumbUrl;
|
||||
|
||||
if (urls[currentIndex].type == 'video') {
|
||||
await _saveVideo(url);
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
await _saveImage(url);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}),
|
||||
if (StreamChat.of(context).user.id == message.user.id)
|
||||
_buildButton(
|
||||
context,
|
||||
'Delete',
|
||||
StreamSvgIcon.delete(
|
||||
size: 24.0,
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentRed,
|
||||
),
|
||||
() {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context);
|
||||
StreamChat.of(context).client.deleteMessage(
|
||||
message,
|
||||
StreamChannel.of(context).channel.cid,
|
||||
);
|
||||
},
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentRed,
|
||||
),
|
||||
],
|
||||
).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildButton(
|
||||
context, String title, StreamSvgIcon icon, VoidCallback onTap,
|
||||
{Color color}) {
|
||||
var titleStyle = TextStyle(
|
||||
fontSize: 14.5,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
);
|
||||
|
||||
return Material(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: Text(
|
||||
title,
|
||||
style:
|
||||
color == null ? titleStyle : titleStyle.copyWith(color: color),
|
||||
),
|
||||
leading: icon,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _saveImage(String url) async {
|
||||
var response = await Dio()
|
||||
.get(url, options: Options(responseType: ResponseType.bytes));
|
||||
final result = await ImageGallerySaver.saveImage(
|
||||
Uint8List.fromList(response.data),
|
||||
quality: 60,
|
||||
name: "${DateTime.now().millisecondsSinceEpoch}");
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> _saveVideo(String url) async {
|
||||
var appDocDir = await getTemporaryDirectory();
|
||||
var savePath =
|
||||
appDocDir.path + "/${DateTime.now().millisecondsSinceEpoch}.mp4";
|
||||
await Dio().download(url, savePath);
|
||||
final result = await ImageGallerySaver.saveFile(savePath);
|
||||
print(result);
|
||||
}
|
||||
}
|
||||
@@ -1,118 +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;
|
||||
|
||||
const ImageAttachment({
|
||||
Key key,
|
||||
@required this.attachment,
|
||||
@required this.message,
|
||||
@required this.size,
|
||||
this.messageTheme,
|
||||
this.showTitle = true,
|
||||
this.onShowMessage,
|
||||
}) : 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: () {
|
||||
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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,423 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
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:image_gallery_saver/image_gallery_saver.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
class ImageFooter extends StatefulWidget implements PreferredSizeWidget {
|
||||
/// Callback to call when pressing the back button.
|
||||
/// By default it calls [Navigator.pop]
|
||||
final VoidCallback onBackPressed;
|
||||
|
||||
/// Callback to call when the header is tapped.
|
||||
final VoidCallback onTitleTap;
|
||||
|
||||
/// Callback to call when the image is tapped.
|
||||
final VoidCallback onImageTap;
|
||||
|
||||
final int currentPage;
|
||||
final int totalPages;
|
||||
|
||||
final List<Attachment> mediaAttachments;
|
||||
final Message message;
|
||||
|
||||
final List<VideoPackage> videoPackages;
|
||||
final ValueChanged<int> mediaSelectedCallBack;
|
||||
|
||||
/// Creates a channel header
|
||||
ImageFooter({
|
||||
Key key,
|
||||
this.onBackPressed,
|
||||
this.onTitleTap,
|
||||
this.onImageTap,
|
||||
this.currentPage = 0,
|
||||
this.totalPages = 0,
|
||||
this.mediaAttachments,
|
||||
this.message,
|
||||
this.videoPackages,
|
||||
this.mediaSelectedCallBack,
|
||||
}) : preferredSize = Size.fromHeight(kToolbarHeight),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
_ImageFooterState createState() => _ImageFooterState();
|
||||
|
||||
@override
|
||||
final Size preferredSize;
|
||||
}
|
||||
|
||||
class _ImageFooterState extends State<ImageFooter> {
|
||||
bool _userSearchMode = false;
|
||||
TextEditingController _searchController;
|
||||
final TextEditingController _messageController = TextEditingController();
|
||||
final FocusNode _messageFocusNode = FocusNode();
|
||||
|
||||
String _channelNameQuery;
|
||||
|
||||
final List<Channel> _selectedChannels = [];
|
||||
bool _loading = false;
|
||||
|
||||
Timer _debounce;
|
||||
|
||||
Function modalSetStateCallback;
|
||||
|
||||
void _userNameListener() {
|
||||
if (_debounce?.isActive ?? false) _debounce.cancel();
|
||||
_debounce = Timer(const Duration(milliseconds: 350), () {
|
||||
if (mounted && modalSetStateCallback != null) {
|
||||
modalSetStateCallback(() {
|
||||
_channelNameQuery = _searchController.text;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_searchController = TextEditingController()..addListener(_userNameListener);
|
||||
_messageFocusNode.addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController?.clear();
|
||||
_searchController?.removeListener(_userNameListener);
|
||||
_searchController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox.fromSize(
|
||||
size: Size(
|
||||
MediaQuery.of(context).size.width,
|
||||
MediaQuery.of(context).padding.bottom + widget.preferredSize.height,
|
||||
),
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: BottomAppBar(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.icon_SHARE(
|
||||
size: 24.0,
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
onPressed: () async {
|
||||
final attachment =
|
||||
widget.mediaAttachments[widget.currentPage];
|
||||
var url = attachment.imageUrl ??
|
||||
attachment.assetUrl ??
|
||||
attachment.thumbUrl;
|
||||
var type = attachment.type == 'image'
|
||||
? 'jpg'
|
||||
: url?.split('?')?.first?.split('.')?.last ?? 'jpg';
|
||||
var request = await HttpClient().getUrl(Uri.parse(url));
|
||||
var response = await request.close();
|
||||
var bytes =
|
||||
await consolidateHttpClientResponseBytes(response);
|
||||
await Share.file('File', 'image.$type', bytes, 'image/$type');
|
||||
},
|
||||
),
|
||||
InkWell(
|
||||
onTap: widget.onTitleTap,
|
||||
child: Container(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'${widget.currentPage + 1} of ${widget.totalPages}',
|
||||
style:
|
||||
StreamChatTheme.of(context).textTheme.headlineBold,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.Icon_grid(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
onPressed: () => _showPhotosModal(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPhotosModal(context) {
|
||||
var videoAttachments = widget.mediaAttachments
|
||||
.where((element) => element.type == 'video')
|
||||
.toList();
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
),
|
||||
builder: (context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(16.0),
|
||||
topLeft: Radius.circular(16.0),
|
||||
)),
|
||||
child: Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
'Photos',
|
||||
style:
|
||||
StreamChatTheme.of(context).textTheme.headlineBold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: IconButton(
|
||||
icon: StreamSvgIcon.close(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
onPressed: () => Navigator.maybePop(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
child: GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, position) {
|
||||
if (widget.mediaAttachments[position].type == 'video') {
|
||||
var controllerPackage = widget.videoPackages[
|
||||
videoAttachments
|
||||
.indexOf(widget.mediaAttachments[position])];
|
||||
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
widget.mediaSelectedCallBack(position);
|
||||
},
|
||||
child: FittedBox(
|
||||
fit: BoxFit.cover,
|
||||
child: Chewie(
|
||||
controller: controllerPackage.chewieController,
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
widget.mediaSelectedCallBack(position);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(1.0),
|
||||
child: AspectRatio(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: widget
|
||||
.mediaAttachments[position].imageUrl ??
|
||||
widget.mediaAttachments[position].assetUrl ??
|
||||
widget.mediaAttachments[position].thumbUrl,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
aspectRatio: 1.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
itemCount: widget.mediaAttachments.length,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildShareTextInputSection(modalSetState) {
|
||||
return Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: BottomAppBar(
|
||||
child: Container(
|
||||
height: 40.0,
|
||||
margin: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 8,
|
||||
),
|
||||
child: _loading
|
||||
? Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
)
|
||||
: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _messageController,
|
||||
focusNode: _messageFocusNode,
|
||||
onChanged: (val) {
|
||||
modalSetState(() {});
|
||||
},
|
||||
onTap: () {
|
||||
modalSetState(() {});
|
||||
setState(() {});
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
prefixText: ' ',
|
||||
hintText: 'Add a comment',
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
borderSide: BorderSide(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.16),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
borderSide: BorderSide(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.16),
|
||||
)),
|
||||
contentPadding: const EdgeInsets.all(0),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
IconTheme(
|
||||
data: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.messageInputButtonIconTheme,
|
||||
child: IconButton(
|
||||
onPressed: () async {
|
||||
modalSetState(() => _loading = true);
|
||||
await sendMessage();
|
||||
modalSetState(() => _loading = false);
|
||||
},
|
||||
splashRadius: 24,
|
||||
visualDensity: VisualDensity.compact,
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
icon: Transform.rotate(
|
||||
angle: -pi / 2,
|
||||
child: StreamSvgIcon.Icon_send_message(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Sends the current message
|
||||
Future sendMessage() async {
|
||||
var text = _messageController.text.trim();
|
||||
|
||||
final attachments = widget.message.attachments;
|
||||
|
||||
_messageController.clear();
|
||||
|
||||
for (var channel in _selectedChannels) {
|
||||
final message = Message(
|
||||
text: text,
|
||||
attachments: [attachments[widget.currentPage]],
|
||||
);
|
||||
|
||||
await channel.sendMessage(message);
|
||||
}
|
||||
|
||||
_selectedChannels.clear();
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
Future<void> _saveImage(String url) async {
|
||||
var response = await Dio()
|
||||
.get(url, options: Options(responseType: ResponseType.bytes));
|
||||
final result = await ImageGallerySaver.saveImage(
|
||||
Uint8List.fromList(response.data),
|
||||
quality: 60,
|
||||
name: "${DateTime.now().millisecondsSinceEpoch}");
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<void> _saveVideo(String url) async {
|
||||
var appDocDir = await getTemporaryDirectory();
|
||||
var savePath =
|
||||
appDocDir.path + "/${DateTime.now().millisecondsSinceEpoch}.mp4";
|
||||
await Dio().download(url, savePath);
|
||||
final result = await ImageGallerySaver.saveFile(savePath);
|
||||
print(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Used for clipping textfield prefix icon
|
||||
class IconClipper extends CustomClipper<Path> {
|
||||
@override
|
||||
Path getClip(Size size) {
|
||||
var leftX = size.width / 5;
|
||||
var rightX = 4 * size.width / 5;
|
||||
var topY = size.height / 5;
|
||||
var bottomY = 4 * size.height / 5;
|
||||
|
||||
final path = Path();
|
||||
path.moveTo(leftX, topY);
|
||||
path.lineTo(leftX, bottomY);
|
||||
path.lineTo(rightX, bottomY);
|
||||
path.lineTo(rightX, topY);
|
||||
path.lineTo(leftX, topY);
|
||||
path.lineTo(0.0, 0.0);
|
||||
path.close();
|
||||
return path;
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(CustomClipper oldClipper) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/full_screen_media.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
class ImageGroup extends StatelessWidget {
|
||||
const ImageGroup({
|
||||
Key key,
|
||||
@required this.images,
|
||||
@required this.message,
|
||||
@required this.size,
|
||||
this.onShowMessage,
|
||||
}) : super(key: key);
|
||||
|
||||
final List<Attachment> images;
|
||||
final Message message;
|
||||
final Size size;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints.loose(size),
|
||||
child: Flex(
|
||||
direction: Axis.vertical,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
flex: 1,
|
||||
fit: FlexFit.tight,
|
||||
child: Flex(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
direction: Axis.horizontal,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
fit: FlexFit.tight,
|
||||
child: _buildImage(context, 0),
|
||||
),
|
||||
Flexible(
|
||||
flex: 1,
|
||||
fit: FlexFit.tight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 2.0),
|
||||
child: _buildImage(context, 1),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (images.length >= 3)
|
||||
Flexible(
|
||||
fit: FlexFit.tight,
|
||||
flex: 1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 2.0),
|
||||
child: Flex(
|
||||
direction: Axis.horizontal,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
fit: FlexFit.tight,
|
||||
flex: 1,
|
||||
child: _buildImage(context, 2),
|
||||
),
|
||||
if (images.length >= 4)
|
||||
Flexible(
|
||||
fit: FlexFit.tight,
|
||||
flex: 1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 2.0),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: <Widget>[
|
||||
_buildImage(context, 3),
|
||||
if (images.length > 4)
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
onTap: () => _onTap(context, 3),
|
||||
child: Material(
|
||||
color: Colors.black38,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'+ ${images.length - 4}',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 26,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onTap(
|
||||
BuildContext context, [
|
||||
int index,
|
||||
]) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments: images,
|
||||
startIndex: index,
|
||||
userName: message.user.name,
|
||||
sentAt: message.createdAt,
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'image_actions_modal.dart';
|
||||
import 'stream_channel.dart';
|
||||
|
||||
class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
/// True if this header shows the leading back button
|
||||
final bool showBackButton;
|
||||
|
||||
/// Callback to call when pressing the back button.
|
||||
/// By default it calls [Navigator.pop]
|
||||
final VoidCallback onBackPressed;
|
||||
|
||||
/// Callback to call when pressing the show message button.
|
||||
final VoidCallback onShowMessage;
|
||||
|
||||
/// Callback to call when the header is tapped.
|
||||
final VoidCallback onTitleTap;
|
||||
|
||||
/// Callback to call when the image is tapped.
|
||||
final VoidCallback onImageTap;
|
||||
|
||||
final Message message;
|
||||
|
||||
final String userName;
|
||||
final String sentAt;
|
||||
|
||||
final List<Attachment> urls;
|
||||
final currentIndex;
|
||||
|
||||
/// Creates a channel header
|
||||
ImageHeader({
|
||||
Key key,
|
||||
this.message,
|
||||
this.urls,
|
||||
this.currentIndex,
|
||||
this.showBackButton = true,
|
||||
this.onBackPressed,
|
||||
this.onShowMessage,
|
||||
this.onTitleTap,
|
||||
this.onImageTap,
|
||||
this.userName = '',
|
||||
this.sentAt = '',
|
||||
}) : preferredSize = Size.fromHeight(kToolbarHeight),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBar(
|
||||
brightness: Theme.of(context).brightness,
|
||||
elevation: 1,
|
||||
leading: showBackButton
|
||||
? IconButton(
|
||||
icon: StreamSvgIcon.close(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
size: 24.0,
|
||||
),
|
||||
onPressed: onBackPressed,
|
||||
)
|
||||
: SizedBox(),
|
||||
backgroundColor:
|
||||
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: StreamSvgIcon.Icon_menu_point_v(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
onPressed: () {
|
||||
_showMessageActionModalBottomSheet(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
centerTitle: true,
|
||||
title: InkWell(
|
||||
onTap: onTitleTap,
|
||||
child: Container(
|
||||
height: preferredSize.height,
|
||||
width: preferredSize.width,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
userName,
|
||||
style: StreamChatTheme.of(context).textTheme.headlineBold,
|
||||
),
|
||||
Text(
|
||||
sentAt,
|
||||
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
final Size preferredSize;
|
||||
|
||||
void _showMessageActionModalBottomSheet(BuildContext context) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
|
||||
builder: (context) {
|
||||
return StreamChannel(
|
||||
channel: channel,
|
||||
child: ImageActionsModal(
|
||||
userName: userName,
|
||||
sentAt: sentAt,
|
||||
message: message,
|
||||
urls: urls,
|
||||
currentIndex: currentIndex,
|
||||
onShowMessage: onShowMessage,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
enum _LoadingStatus { LOADING, STABLE }
|
||||
|
||||
/// A widget that wraps a [Widget] and will trigger [onEndOfPage]/[onStartOfPage] when it
|
||||
/// reaches the bottom/start of the list
|
||||
class LazyLoadScrollView extends StatefulWidget {
|
||||
/// The [Widget] that this widget watches for changes on
|
||||
final Widget child;
|
||||
|
||||
/// Called when the [child] reaches the start of the list
|
||||
final AsyncCallback onStartOfPage;
|
||||
|
||||
/// Called when the [child] reaches the end of the list
|
||||
final AsyncCallback onEndOfPage;
|
||||
|
||||
/// Called when the list scrolling starts
|
||||
final VoidCallback onPageScrollStart;
|
||||
|
||||
/// Called when the list scrolling ends
|
||||
final VoidCallback onPageScrollEnd;
|
||||
|
||||
/// Called every time the [child] is in-between the list
|
||||
final VoidCallback onInBetweenOfPage;
|
||||
|
||||
/// The offset to take into account when triggering [onEndOfPage]/[onStartOfPage] in pixels
|
||||
final double scrollOffset;
|
||||
|
||||
/// Used to determine if loading of new data has finished. You should use set this if you aren't using a FutureBuilder or StreamBuilder
|
||||
final bool isLoading;
|
||||
|
||||
/// Initiates a LazyLoadScrollView widget
|
||||
const LazyLoadScrollView({
|
||||
Key key,
|
||||
@required this.child,
|
||||
this.onStartOfPage,
|
||||
this.onEndOfPage,
|
||||
this.onPageScrollStart,
|
||||
this.onPageScrollEnd,
|
||||
this.onInBetweenOfPage,
|
||||
this.isLoading = false,
|
||||
this.scrollOffset = 100,
|
||||
}) : assert(child != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _LazyLoadScrollViewState();
|
||||
}
|
||||
|
||||
class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
|
||||
_LoadingStatus _loadMoreStatus = _LoadingStatus.STABLE;
|
||||
double _scrollPosition = 0.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return NotificationListener(
|
||||
child: widget.child,
|
||||
onNotification: _onNotification,
|
||||
);
|
||||
}
|
||||
|
||||
bool _onNotification(Notification notification) {
|
||||
if (notification is ScrollStartNotification) {
|
||||
if (widget.onPageScrollStart != null) {
|
||||
widget.onPageScrollStart();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (notification is ScrollEndNotification) {
|
||||
if (widget.onPageScrollEnd != null) {
|
||||
widget.onPageScrollEnd();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (notification is ScrollUpdateNotification) {
|
||||
final pixels = notification.metrics.pixels;
|
||||
final maxScrollExtent = notification.metrics.maxScrollExtent;
|
||||
final minScrollExtent = notification.metrics.minScrollExtent;
|
||||
final scrollOffset = widget.scrollOffset;
|
||||
|
||||
if (pixels > (minScrollExtent + scrollOffset) &&
|
||||
pixels < (maxScrollExtent - scrollOffset)) {
|
||||
if (widget.onInBetweenOfPage != null) {
|
||||
widget.onInBetweenOfPage();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
final extentBefore = notification.metrics.extentBefore;
|
||||
final extentAfter = notification.metrics.extentAfter;
|
||||
final scrollingDown = _scrollPosition < pixels;
|
||||
|
||||
if (scrollOffset == null || scrollOffset == 0) {
|
||||
if (extentAfter == 0) {
|
||||
_onEndOfPage();
|
||||
}
|
||||
if (extentBefore == 0) {
|
||||
_onStartOfPage();
|
||||
}
|
||||
} else {
|
||||
if (scrollingDown) {
|
||||
if (extentAfter <= scrollOffset) {
|
||||
_onEndOfPage();
|
||||
}
|
||||
} else {
|
||||
if (extentBefore <= scrollOffset) {
|
||||
_onStartOfPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
_scrollPosition = pixels;
|
||||
return true;
|
||||
}
|
||||
if (notification is OverscrollNotification) {
|
||||
if (notification.overscroll > 0) {
|
||||
_onEndOfPage();
|
||||
}
|
||||
if (notification.overscroll < 0) {
|
||||
_onStartOfPage();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _onEndOfPage() {
|
||||
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) {
|
||||
_loadMoreStatus = _LoadingStatus.LOADING;
|
||||
if (widget.onEndOfPage != null) {
|
||||
widget.onEndOfPage().whenComplete(() {
|
||||
_loadMoreStatus = _LoadingStatus.STABLE;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _onStartOfPage() {
|
||||
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) {
|
||||
_loadMoreStatus = _LoadingStatus.LOADING;
|
||||
if (widget.onStartOfPage != null) {
|
||||
widget.onStartOfPage().whenComplete(() {
|
||||
_loadMoreStatus = _LoadingStatus.STABLE;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
|
||||
extension on Duration {
|
||||
String format() {
|
||||
final s = '$this'.split('.')[0].padLeft(8, '0');
|
||||
if (s.startsWith('00:')) {
|
||||
return s.replaceFirst('00:', '');
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
class MediaListView extends StatefulWidget {
|
||||
final List<String> selectedIds;
|
||||
final void Function(AssetEntity media) onSelect;
|
||||
|
||||
const MediaListView({
|
||||
Key key,
|
||||
this.selectedIds = const [],
|
||||
this.onSelect,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_MediaListViewState createState() => _MediaListViewState();
|
||||
}
|
||||
|
||||
class _MediaListViewState extends State<MediaListView> {
|
||||
final _media = <AssetEntity>[];
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
int _currentPage = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LazyLoadScrollView(
|
||||
onEndOfPage: () async => _getMedia(),
|
||||
child: GridView.builder(
|
||||
itemCount: _media.length,
|
||||
controller: _scrollController,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
),
|
||||
itemBuilder: (
|
||||
context,
|
||||
position,
|
||||
) {
|
||||
final media = _media.elementAt(position);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 1.0, vertical: 1.0),
|
||||
child: InkWell(
|
||||
child: Stack(
|
||||
children: [
|
||||
AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: FadeInImage(
|
||||
fadeInDuration: Duration(milliseconds: 300),
|
||||
placeholder: AssetImage(
|
||||
'images/placeholder.png',
|
||||
package: 'stream_chat_flutter',
|
||||
),
|
||||
image: MediaThumbnailProvider(
|
||||
media: media,
|
||||
),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: AnimatedOpacity(
|
||||
duration: Duration(milliseconds: 300),
|
||||
opacity: widget.selectedIds.any((id) => id == media.id)
|
||||
? 1.0
|
||||
: 0.0,
|
||||
child: Container(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5),
|
||||
alignment: Alignment.topRight,
|
||||
padding: const EdgeInsets.only(
|
||||
top: 8,
|
||||
right: 8,
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 12,
|
||||
backgroundColor:
|
||||
StreamChatTheme.of(context).colorTheme.white,
|
||||
child: StreamSvgIcon.check(
|
||||
size: 24,
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (media.type == AssetType.video) ...[
|
||||
Positioned(
|
||||
left: 8,
|
||||
bottom: 10,
|
||||
child: SvgPicture.asset(
|
||||
'svgs/video_call_icon.svg',
|
||||
package: 'stream_chat_flutter',
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
right: 4,
|
||||
bottom: 10,
|
||||
child: Text(
|
||||
media.videoDuration.format(),
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
if (widget.onSelect != null) {
|
||||
widget.onSelect(media);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_getMedia();
|
||||
}
|
||||
|
||||
void _getMedia() async {
|
||||
final assetList = await PhotoManager.getAssetPathList(
|
||||
hasAll: true,
|
||||
).then((value) {
|
||||
if (value?.isNotEmpty == true) {
|
||||
return value.singleWhere((element) => element.isAll);
|
||||
}
|
||||
});
|
||||
|
||||
if (assetList == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final media = await assetList.getAssetListPaged(_currentPage, 50);
|
||||
|
||||
if (!media.isEmpty) {
|
||||
setState(() {
|
||||
_media.addAll(media);
|
||||
});
|
||||
}
|
||||
++_currentPage;
|
||||
}
|
||||
}
|
||||
|
||||
class MediaThumbnailProvider extends ImageProvider<MediaThumbnailProvider> {
|
||||
const MediaThumbnailProvider({
|
||||
@required this.media,
|
||||
}) : assert(media != null);
|
||||
|
||||
final AssetEntity media;
|
||||
|
||||
@override
|
||||
ImageStreamCompleter load(key, decode) {
|
||||
return MultiFrameImageStreamCompleter(
|
||||
codec: _loadAsync(key, decode),
|
||||
scale: 1.0,
|
||||
informationCollector: () sync* {
|
||||
yield ErrorDescription('Id: ${media?.id}');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<ui.Codec> _loadAsync(
|
||||
MediaThumbnailProvider key, DecoderCallback decode) async {
|
||||
assert(key == this);
|
||||
final bytes = await media.thumbData;
|
||||
if (bytes?.isNotEmpty != true) return null;
|
||||
|
||||
return await decode(bytes);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MediaThumbnailProvider> obtainKey(ImageConfiguration configuration) {
|
||||
return SynchronousFuture<MediaThumbnailProvider>(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
final MediaThumbnailProvider typedOther = other;
|
||||
return media?.id == typedOther.media?.id;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => media?.id?.hashCode ?? 0;
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType("${media?.id}")';
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import 'package:http_parser/http_parser.dart' as httpParser;
|
||||
import 'package:mime/mime.dart';
|
||||
|
||||
class MediaUtils {
|
||||
static httpParser.MediaType getMimeType(String filename) {
|
||||
httpParser.MediaType mimeType;
|
||||
if (filename != null) {
|
||||
if (filename.toLowerCase().endsWith('heic')) {
|
||||
mimeType = httpParser.MediaType.parse('image/heic');
|
||||
} else {
|
||||
mimeType = httpParser.MediaType.parse(lookupMimeType(filename));
|
||||
}
|
||||
}
|
||||
|
||||
return mimeType;
|
||||
}
|
||||
}
|
||||
@@ -1,392 +0,0 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/reaction_picker.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_channel.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
|
||||
import 'message_input.dart';
|
||||
import 'message_widget.dart';
|
||||
import 'stream_chat.dart';
|
||||
import 'stream_chat_theme.dart';
|
||||
|
||||
class MessageActionsModal extends StatelessWidget {
|
||||
final Widget Function(BuildContext, Message) editMessageInputBuilder;
|
||||
final void Function(Message) onThreadReplyTap;
|
||||
final void Function(Message) onReplyTap;
|
||||
final Message message;
|
||||
final MessageTheme messageTheme;
|
||||
final bool showReactions;
|
||||
final bool showDeleteMessage;
|
||||
final bool showCopyMessage;
|
||||
final bool showEditMessage;
|
||||
final bool showResendMessage;
|
||||
final bool showReply;
|
||||
final bool showThreadReply;
|
||||
final bool reverse;
|
||||
final ShapeBorder messageShape;
|
||||
final DisplayWidget showUserAvatar;
|
||||
|
||||
const MessageActionsModal({
|
||||
Key key,
|
||||
@required this.message,
|
||||
@required this.messageTheme,
|
||||
this.showReactions = true,
|
||||
this.showDeleteMessage = true,
|
||||
this.showEditMessage = true,
|
||||
this.onReplyTap,
|
||||
this.onThreadReplyTap,
|
||||
this.showCopyMessage = true,
|
||||
this.showReply = true,
|
||||
this.showResendMessage = true,
|
||||
this.showThreadReply = true,
|
||||
this.showUserAvatar = DisplayWidget.show,
|
||||
this.editMessageInputBuilder,
|
||||
this.messageShape,
|
||||
this.reverse = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final user = StreamChat.of(context).user;
|
||||
|
||||
final roughMaxSize = 2 * size.width / 3;
|
||||
var messageTextLength = message.text.length;
|
||||
if (message.quotedMessage != null) {
|
||||
var quotedMessageLength = message.quotedMessage.text.length + 40;
|
||||
if (message.quotedMessage.attachments?.isNotEmpty == true) {
|
||||
quotedMessageLength += 40;
|
||||
}
|
||||
if (quotedMessageLength > messageTextLength) {
|
||||
messageTextLength = quotedMessageLength;
|
||||
}
|
||||
}
|
||||
final roughSentenceSize =
|
||||
messageTextLength * messageTheme.messageText.fontSize * 1.2;
|
||||
final divFactor = message.attachments?.isNotEmpty == true
|
||||
? 1
|
||||
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => Navigator.maybePop(context),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: 10,
|
||||
sigmaY: 10,
|
||||
),
|
||||
child: Container(
|
||||
color: StreamChatTheme.of(context).colorTheme.overlay,
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
if (showReactions &&
|
||||
(message.status == MessageSendingStatus.SENT ||
|
||||
message.status == null))
|
||||
Align(
|
||||
alignment: Alignment(
|
||||
user.id == message.user.id
|
||||
? (divFactor > 1.0 ? 0.0 : (1.0 - divFactor))
|
||||
: (divFactor > 1.0 ? 0.0 : -(1.0 - divFactor)),
|
||||
0.0),
|
||||
child: ReactionPicker(
|
||||
message: message,
|
||||
messageTheme: messageTheme,
|
||||
),
|
||||
),
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
duration: Duration(milliseconds: 300),
|
||||
builder: (context, val, snapshot) {
|
||||
return Transform.scale(
|
||||
scale: val,
|
||||
child: IgnorePointer(
|
||||
child: MessageWidget(
|
||||
key: Key('MessageWidget'),
|
||||
reverse: reverse,
|
||||
message: message.copyWith(
|
||||
text: message.text.length > 200
|
||||
? '${message.text.substring(0, 200)}...'
|
||||
: message.text,
|
||||
),
|
||||
messageTheme: messageTheme,
|
||||
showReactions: false,
|
||||
showUsername: false,
|
||||
showThreadReplyIndicator: false,
|
||||
showReplyIndicator: false,
|
||||
showUserAvatar: showUserAvatar,
|
||||
showTimestamp: false,
|
||||
translateUserAvatar: false,
|
||||
showReactionPickerIndicator: showReactions &&
|
||||
(message.status ==
|
||||
MessageSendingStatus.SENT ||
|
||||
message.status == null),
|
||||
showInChannelIndicator: false,
|
||||
showSendingIndicator: DisplayWidget.gone,
|
||||
shape: messageShape,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(height: 8),
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
duration: Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
builder: (context, val, wid) {
|
||||
return Transform(
|
||||
transform: Matrix4.identity()
|
||||
..scale(val)
|
||||
..rotateZ(-1.0 + val),
|
||||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 48.0,
|
||||
),
|
||||
child: Material(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.whiteSnow,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.stretch,
|
||||
children: ListTile.divideTiles(
|
||||
context: context,
|
||||
tiles: [
|
||||
if (showReply &&
|
||||
(message.status ==
|
||||
MessageSendingStatus.SENT ||
|
||||
message.status == null) &&
|
||||
message.parentId == null)
|
||||
_buildReplyButton(context),
|
||||
if (showThreadReply &&
|
||||
(message.status ==
|
||||
MessageSendingStatus.SENT ||
|
||||
message.status == null) &&
|
||||
message.parentId == null)
|
||||
_buildThreadReplyButton(context),
|
||||
if (showResendMessage)
|
||||
_buildResendMessage(context),
|
||||
if (showEditMessage)
|
||||
_buildEditMessage(context),
|
||||
if (showDeleteMessage)
|
||||
_buildDeleteButton(context),
|
||||
if (showCopyMessage)
|
||||
_buildCopyButton(context),
|
||||
],
|
||||
).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReplyButton(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
'Reply',
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
leading: StreamSvgIcon.reply(
|
||||
color: StreamChatTheme.of(context).primaryIconTheme.color,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
if (onReplyTap != null) {
|
||||
onReplyTap(message);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDeleteButton(BuildContext context) {
|
||||
final isDeleteFailed = message.status == MessageSendingStatus.FAILED_DELETE;
|
||||
return ListTile(
|
||||
title: Text(
|
||||
isDeleteFailed ? 'Retry deleting message' : 'Delete message',
|
||||
style:
|
||||
Theme.of(context).textTheme.headline6.copyWith(color: Colors.red),
|
||||
),
|
||||
leading: StreamSvgIcon.delete(
|
||||
color: Colors.red,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
StreamChat.of(context).client.deleteMessage(
|
||||
message,
|
||||
StreamChannel.of(context).channel.cid,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCopyButton(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
'Copy message',
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
leading: StreamSvgIcon.copy(
|
||||
color: StreamChatTheme.of(context).primaryIconTheme.color,
|
||||
),
|
||||
onTap: () async {
|
||||
await Clipboard.setData(ClipboardData(text: message.text));
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEditMessage(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
'Edit message',
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
leading: StreamSvgIcon.edit(
|
||||
color: StreamChatTheme.of(context).primaryIconTheme.color,
|
||||
),
|
||||
onTap: () async {
|
||||
Navigator.pop(context);
|
||||
_showEditBottomSheet(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildResendMessage(BuildContext context) {
|
||||
final isUpdateFailed = message.status == MessageSendingStatus.FAILED_UPDATE;
|
||||
return ListTile(
|
||||
title: Text(
|
||||
isUpdateFailed ? 'Resend edited message' : 'Resend',
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
leading: StreamSvgIcon.circle_up(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
final client = StreamChat.of(context).client;
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
if (isUpdateFailed) {
|
||||
client.updateMessage(message, channel.cid);
|
||||
} else {
|
||||
channel.sendMessage(message);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showEditBottomSheet(BuildContext context) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
elevation: 2,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
topRight: Radius.circular(16),
|
||||
),
|
||||
),
|
||||
builder: (context) {
|
||||
return StreamChannel(
|
||||
channel: channel,
|
||||
child: Flex(
|
||||
direction: Axis.vertical,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: StreamSvgIcon.edit(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.greyGainsboro,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Edit Message',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: StreamSvgIcon.close_small(),
|
||||
onPressed: Navigator.of(context).pop,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: editMessageInputBuilder != null
|
||||
? editMessageInputBuilder(context, message)
|
||||
: MessageInput(
|
||||
editMessage: message,
|
||||
preMessageSending: (m) {
|
||||
FocusScope.of(context).unfocus();
|
||||
Navigator.pop(context);
|
||||
return m;
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildThreadReplyButton(BuildContext context) {
|
||||
return ListTile(
|
||||
title: Text(
|
||||
'Thread reply',
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
leading: StreamSvgIcon.thread(
|
||||
color: StreamChatTheme.of(context).primaryIconTheme.color,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context);
|
||||
if (onThreadReplyTap != null) {
|
||||
onThreadReplyTap(message);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,940 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart';
|
||||
import 'package:stream_chat_flutter/src/message_widget.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/system_message.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'date_divider.dart';
|
||||
import 'stream_channel.dart';
|
||||
import 'swipeable.dart';
|
||||
|
||||
typedef MessageBuilder = Widget Function(
|
||||
BuildContext,
|
||||
MessageDetails,
|
||||
List<Message>,
|
||||
);
|
||||
typedef ParentMessageBuilder = Widget Function(
|
||||
BuildContext,
|
||||
Message,
|
||||
);
|
||||
typedef ThreadBuilder = Widget Function(BuildContext context, Message parent);
|
||||
typedef ThreadTapCallback = void Function(Message, Widget);
|
||||
|
||||
typedef OnMessageSwiped = void Function(Message);
|
||||
typedef ReplyTapCallback = void Function(Message);
|
||||
|
||||
class MessageDetails {
|
||||
/// True if the message belongs to the current user
|
||||
bool isMyMessage;
|
||||
|
||||
/// True if the user message is the same of the previous message
|
||||
bool isLastUser;
|
||||
|
||||
/// True if the user message is the same of the next message
|
||||
bool isNextUser;
|
||||
|
||||
/// The message
|
||||
Message message;
|
||||
|
||||
/// The index of the message
|
||||
int index;
|
||||
|
||||
MessageDetails(
|
||||
BuildContext context,
|
||||
this.message,
|
||||
List<Message> messages,
|
||||
this.index,
|
||||
) {
|
||||
isMyMessage = message.user.id == StreamChat.of(context).user.id;
|
||||
isLastUser = index + 1 < messages.length &&
|
||||
message.user.id == messages[index + 1]?.user?.id;
|
||||
isNextUser =
|
||||
index - 1 >= 0 && message.user.id == messages[index - 1]?.user?.id;
|
||||
}
|
||||
}
|
||||
|
||||
/// 
|
||||
/// 
|
||||
///
|
||||
/// It shows the list of messages of the current channel.
|
||||
///
|
||||
/// ```dart
|
||||
/// class ChannelPage extends StatelessWidget {
|
||||
/// const ChannelPage({
|
||||
/// Key key,
|
||||
/// }) : super(key: key);
|
||||
///
|
||||
/// @override
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return Scaffold(
|
||||
/// appBar: ChannelHeader(),
|
||||
/// body: Column(
|
||||
/// children: <Widget>[
|
||||
/// Expanded(
|
||||
/// child: MessageListView(
|
||||
/// threadBuilder: (_, parentMessage) {
|
||||
/// return ThreadPage(
|
||||
/// parent: parentMessage,
|
||||
/// );
|
||||
/// },
|
||||
/// ),
|
||||
/// ),
|
||||
/// MessageInput(),
|
||||
/// ],
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
/// Make sure to have a [StreamChannel] ancestor in order to provide the information about the channels.
|
||||
/// The widget uses a [ListView.custom] to render the list of channels.
|
||||
///
|
||||
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme].
|
||||
/// Modify it to change the widget appearance.
|
||||
class MessageListView extends StatefulWidget {
|
||||
/// Instantiate a new MessageListView
|
||||
MessageListView({
|
||||
Key key,
|
||||
this.showScrollToBottom = true,
|
||||
this.messageBuilder,
|
||||
this.parentMessageBuilder,
|
||||
this.parentMessage,
|
||||
this.threadBuilder,
|
||||
this.onThreadTap,
|
||||
this.onReplyTap,
|
||||
this.dateDividerBuilder,
|
||||
this.scrollPhysics = const ClampingScrollPhysics(),
|
||||
this.initialScrollIndex,
|
||||
this.initialAlignment,
|
||||
this.scrollController,
|
||||
this.itemPositionListener,
|
||||
this.onMessageSwiped,
|
||||
this.highlightInitialMessage = false,
|
||||
this.messageHighlightColor,
|
||||
this.onShowMessage,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Function used to build a custom message widget
|
||||
final MessageBuilder messageBuilder;
|
||||
|
||||
/// Function used to build a custom parent message widget
|
||||
final ParentMessageBuilder parentMessageBuilder;
|
||||
|
||||
/// Function used to build a custom thread widget
|
||||
final ThreadBuilder threadBuilder;
|
||||
|
||||
/// Function called when tapping on a thread
|
||||
/// By default it calls [Navigator.push] using the widget built using [threadBuilder]
|
||||
final ThreadTapCallback onThreadTap;
|
||||
|
||||
/// If true will show a scroll to bottom message when there are new messages and the scroll offset is not zero
|
||||
final bool showScrollToBottom;
|
||||
|
||||
/// Parent message in case of a thread
|
||||
final Message parentMessage;
|
||||
|
||||
/// Builder used to render date dividers
|
||||
final Widget Function(DateTime) dateDividerBuilder;
|
||||
|
||||
/// Index of an item to initially align within the viewport.
|
||||
final int initialScrollIndex;
|
||||
|
||||
/// Determines where the leading edge of the item at [initialScrollIndex]
|
||||
/// should be placed.
|
||||
final double initialAlignment;
|
||||
|
||||
/// Controller for jumping or scrolling to an item.
|
||||
final ItemScrollController scrollController;
|
||||
|
||||
/// Provides a listenable iterable of [itemPositions] of items that are on
|
||||
/// screen and their locations.
|
||||
final ItemPositionsListener itemPositionListener;
|
||||
|
||||
/// The ScrollPhysics used by the ListView
|
||||
final ScrollPhysics scrollPhysics;
|
||||
|
||||
/// Called when message item gets swiped
|
||||
final OnMessageSwiped onMessageSwiped;
|
||||
|
||||
///
|
||||
final ReplyTapCallback onReplyTap;
|
||||
|
||||
/// If true the list will highlight the initialMessage if there is any.
|
||||
///
|
||||
/// Also See [StreamChannel]
|
||||
final bool highlightInitialMessage;
|
||||
|
||||
/// Color used while highlighting initial message
|
||||
final Color messageHighlightColor;
|
||||
|
||||
final ShowMessageCallback onShowMessage;
|
||||
|
||||
@override
|
||||
_MessageListViewState createState() => _MessageListViewState();
|
||||
}
|
||||
|
||||
class _MessageListViewState extends State<MessageListView> {
|
||||
ItemScrollController _scrollController;
|
||||
bool _bottomWasVisible = false;
|
||||
Function _onThreadTap;
|
||||
bool _showScrollToBottom = false;
|
||||
ItemPositionsListener _itemPositionListener;
|
||||
int _messageListLength;
|
||||
StreamChannelState streamChannel;
|
||||
|
||||
int get _initialIndex {
|
||||
if (widget.initialScrollIndex != null) return widget.initialScrollIndex;
|
||||
if (streamChannel.initialMessageId != null) {
|
||||
final messages = streamChannel.channel.state.messages;
|
||||
final totalMessages = messages.length;
|
||||
final messageIndex = messages.indexWhere((e) {
|
||||
return e.id == streamChannel.initialMessageId;
|
||||
});
|
||||
final index = totalMessages - messageIndex;
|
||||
if (index != 0) return index - 1;
|
||||
return index;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
double get _initialAlignment {
|
||||
if (widget.initialAlignment != null) return widget.initialAlignment;
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool _isInitialMessage(String id) {
|
||||
return streamChannel.initialMessageId == id;
|
||||
}
|
||||
|
||||
bool get _upToDate => streamChannel.channel.state.isUpToDate;
|
||||
|
||||
bool _topPaginationActive = false;
|
||||
bool _bottomPaginationActive = false;
|
||||
|
||||
int initialIndex;
|
||||
double initialAlignment;
|
||||
|
||||
List<Message> messages = <Message>[];
|
||||
|
||||
bool initialMessageHighlightComplete = false;
|
||||
|
||||
bool _inBetweenList = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final messagesStream = widget.parentMessage != null
|
||||
? streamChannel.channel.state.threadsStream
|
||||
.where((threads) => threads.containsKey(widget.parentMessage.id))
|
||||
.map((threads) => threads[widget.parentMessage.id])
|
||||
: streamChannel.channel.state?.messagesStream;
|
||||
|
||||
return StreamBuilder<List<Message>>(
|
||||
stream: messagesStream?.map((messages) => messages
|
||||
?.where((e) =>
|
||||
(!e.isDeleted && e.shadowed != true) ||
|
||||
(e.isDeleted &&
|
||||
e.user.id == streamChannel.channel.client.state.user.id))
|
||||
?.toList()),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return Center(
|
||||
child: const CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
final messageList = snapshot.data?.reversed?.toList() ?? [];
|
||||
if (messageList.isEmpty) {
|
||||
if (_upToDate) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'No chats here yet...',
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.footnote
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5)),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
messages = messageList;
|
||||
}
|
||||
|
||||
final newMessagesListLength = messages.length;
|
||||
|
||||
if (_messageListLength != null) {
|
||||
if (_bottomPaginationActive || (_inBetweenList && _upToDate)) {
|
||||
if (_itemPositionListener.itemPositions.value?.isNotEmpty ==
|
||||
true) {
|
||||
final first = _itemPositionListener.itemPositions.value.first;
|
||||
final diff = newMessagesListLength - _messageListLength;
|
||||
if (diff > 0) {
|
||||
initialIndex = first.index + diff;
|
||||
initialAlignment = first.itemLeadingEdge;
|
||||
}
|
||||
}
|
||||
} else if (!_topPaginationActive && _upToDate) {
|
||||
// Reset the index in-case we send any new message
|
||||
initialIndex = 0;
|
||||
initialAlignment = 0;
|
||||
}
|
||||
}
|
||||
|
||||
_messageListLength = newMessagesListLength;
|
||||
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
LazyLoadScrollView(
|
||||
onStartOfPage: () async {
|
||||
_inBetweenList = false;
|
||||
if (!_upToDate) {
|
||||
_topPaginationActive = false;
|
||||
_bottomPaginationActive = true;
|
||||
return _paginateData(
|
||||
streamChannel,
|
||||
QueryDirection.bottom,
|
||||
);
|
||||
}
|
||||
},
|
||||
onEndOfPage: () async {
|
||||
_inBetweenList = false;
|
||||
_topPaginationActive = true;
|
||||
_bottomPaginationActive = false;
|
||||
return _paginateData(
|
||||
streamChannel,
|
||||
QueryDirection.top,
|
||||
);
|
||||
},
|
||||
onInBetweenOfPage: () {
|
||||
_inBetweenList = true;
|
||||
},
|
||||
child: ScrollablePositionedList.builder(
|
||||
key: ValueKey(initialIndex + initialAlignment),
|
||||
itemPositionsListener: _itemPositionListener,
|
||||
addAutomaticKeepAlives: true,
|
||||
initialScrollIndex: initialIndex ?? 0,
|
||||
initialAlignment: initialAlignment ?? 0,
|
||||
physics: widget.scrollPhysics,
|
||||
itemScrollController: _scrollController,
|
||||
reverse: true,
|
||||
itemCount: messages.length +
|
||||
2 +
|
||||
(widget.parentMessage != null ? 1 : 0),
|
||||
itemBuilder: (context, i) {
|
||||
if (i == messages.length + 2) {
|
||||
if (widget.parentMessageBuilder != null) {
|
||||
return widget.parentMessageBuilder(
|
||||
context,
|
||||
widget.parentMessage,
|
||||
);
|
||||
} else {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
buildParentMessage(widget.parentMessage),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.bgGradient,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'${widget.parentMessage.replyCount} ${widget.parentMessage.replyCount == 1 ? 'Reply' : 'Replies'}',
|
||||
textAlign: TextAlign.center,
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.lastMessageAt,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
if (i == messages.length + 1) {
|
||||
return _buildLoadingIndicator(
|
||||
streamChannel,
|
||||
QueryDirection.top,
|
||||
);
|
||||
}
|
||||
if (i == 0) {
|
||||
return _buildLoadingIndicator(
|
||||
streamChannel,
|
||||
QueryDirection.bottom,
|
||||
);
|
||||
}
|
||||
final message = messages[i - 1];
|
||||
final nextMessage = (i - 1) > 0 ? messages[i - 2] : null;
|
||||
|
||||
Widget messageWidget;
|
||||
|
||||
if (i == 1) {
|
||||
messageWidget = _buildBottomMessage(
|
||||
context,
|
||||
message,
|
||||
messages,
|
||||
streamChannel,
|
||||
);
|
||||
} else if (i == messages.length - 1) {
|
||||
messageWidget = _buildTopMessage(
|
||||
context,
|
||||
message,
|
||||
messages,
|
||||
streamChannel,
|
||||
);
|
||||
} else {
|
||||
if (widget.messageBuilder != null) {
|
||||
messageWidget = Builder(
|
||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||
builder: (context) => widget.messageBuilder(
|
||||
context,
|
||||
MessageDetails(
|
||||
context,
|
||||
message,
|
||||
messages,
|
||||
i,
|
||||
),
|
||||
messages),
|
||||
);
|
||||
} else {
|
||||
messageWidget = buildMessage(message, messages, i);
|
||||
}
|
||||
}
|
||||
|
||||
if (nextMessage != null &&
|
||||
!Jiffy(message.createdAt.toLocal()).isSame(
|
||||
nextMessage.createdAt.toLocal(), Units.DAY)) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
messageWidget,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0),
|
||||
child: widget.dateDividerBuilder != null
|
||||
? widget.dateDividerBuilder(
|
||||
nextMessage.createdAt.toLocal())
|
||||
: DateDivider(
|
||||
dateTime: nextMessage.createdAt.toLocal(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return messageWidget;
|
||||
},
|
||||
),
|
||||
),
|
||||
if (widget.showScrollToBottom) _buildScrollToBottom(),
|
||||
Positioned(
|
||||
top: 20.0,
|
||||
child: ValueListenableBuilder<Iterable<ItemPosition>>(
|
||||
valueListenable: _itemPositionListener.itemPositions,
|
||||
builder: (context, values, _) {
|
||||
final items = _itemPositionListener.itemPositions?.value;
|
||||
if (items.isEmpty || messages.isEmpty) {
|
||||
return SizedBox();
|
||||
}
|
||||
|
||||
var index = _getTopElement(values).index;
|
||||
|
||||
if (index > messages.length) {
|
||||
return SizedBox();
|
||||
}
|
||||
|
||||
if (index == messages.length) {
|
||||
index = max(index - 1, 0);
|
||||
}
|
||||
|
||||
return widget.dateDividerBuilder != null
|
||||
? widget.dateDividerBuilder(
|
||||
messages[index].createdAt.toLocal(),
|
||||
)
|
||||
: DateDivider(
|
||||
dateTime: messages[index].createdAt.toLocal(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _paginateData(
|
||||
StreamChannelState channel, QueryDirection direction) {
|
||||
if (widget.parentMessage == null) {
|
||||
return channel.queryMessages(direction: direction);
|
||||
} else {
|
||||
return channel.getReplies(widget.parentMessage.id);
|
||||
}
|
||||
}
|
||||
|
||||
ItemPosition _getTopElement(Iterable<ItemPosition> values) {
|
||||
return values
|
||||
.where((ItemPosition position) => position.itemLeadingEdge < 0.9)
|
||||
.reduce((ItemPosition max, ItemPosition position) =>
|
||||
position.itemLeadingEdge > max.itemLeadingEdge ? position : max);
|
||||
}
|
||||
|
||||
Widget _buildScrollToBottom() {
|
||||
return StreamBuilder<Tuple2<bool, int>>(
|
||||
stream: Rx.combineLatest2(
|
||||
streamChannel.channel.state.isUpToDateStream,
|
||||
streamChannel.channel.state.unreadCountStream,
|
||||
(bool isUpToDate, int unreadCount) => Tuple2(isUpToDate, unreadCount),
|
||||
),
|
||||
builder: (_, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Offstage();
|
||||
} else if (!snapshot.hasData) {
|
||||
return Offstage();
|
||||
}
|
||||
final isUpToDate = snapshot.data.item1;
|
||||
final showScrollToBottom = !isUpToDate || _showScrollToBottom;
|
||||
if (!showScrollToBottom) {
|
||||
return Offstage();
|
||||
}
|
||||
final unreadCount = snapshot.data.item2;
|
||||
final showUnreadCount = unreadCount > 0 &&
|
||||
streamChannel.channel.state.members.any(
|
||||
(e) => e.userId == streamChannel.channel.client.state.user.id);
|
||||
return Positioned(
|
||||
bottom: 8,
|
||||
right: 8,
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
FloatingActionButton(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||
child: StreamSvgIcon.down(
|
||||
color: StreamChatTheme.of(context).colorTheme.black,
|
||||
),
|
||||
onPressed: () {
|
||||
if (unreadCount > 0) {
|
||||
streamChannel.channel.markRead();
|
||||
}
|
||||
if (!_upToDate) {
|
||||
_bottomPaginationActive = false;
|
||||
_topPaginationActive = false;
|
||||
streamChannel.reloadChannel();
|
||||
} else {
|
||||
setState(() => _showScrollToBottom = false);
|
||||
_scrollController.scrollTo(
|
||||
index: 0,
|
||||
duration: Duration(seconds: 1),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (showUnreadCount)
|
||||
Positioned(
|
||||
width: 20,
|
||||
height: 20,
|
||||
left: 10,
|
||||
top: -10,
|
||||
child: CircleAvatar(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(3.0),
|
||||
child: Text(
|
||||
'$unreadCount',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingIndicator(
|
||||
StreamChannelState streamChannel,
|
||||
QueryDirection direction,
|
||||
) {
|
||||
final stream = direction == QueryDirection.top
|
||||
? streamChannel.queryTopMessages
|
||||
: streamChannel.queryBottomMessages;
|
||||
return StreamBuilder<bool>(
|
||||
key: Key('LOADING-INDICATOR'),
|
||||
stream: stream,
|
||||
initialData: false,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Container(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentRed
|
||||
.withOpacity(.2),
|
||||
child: Center(
|
||||
child: Text('Error loading messages'),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!snapshot.data) {
|
||||
if (direction == QueryDirection.top) {
|
||||
return Container(
|
||||
height: 52,
|
||||
width: double.infinity,
|
||||
);
|
||||
}
|
||||
return Offstage();
|
||||
}
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: const CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildTopMessage(
|
||||
BuildContext context,
|
||||
Message message,
|
||||
List<Message> messages,
|
||||
StreamChannelState streamChannel,
|
||||
) {
|
||||
Widget messageWidget;
|
||||
if (widget.messageBuilder != null) {
|
||||
messageWidget = Builder(
|
||||
key: ValueKey<String>('TOP-MESSAGE'),
|
||||
builder: (_) => widget.messageBuilder(
|
||||
context,
|
||||
MessageDetails(
|
||||
context,
|
||||
message,
|
||||
messages,
|
||||
messages.length - 1,
|
||||
),
|
||||
messages,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
messageWidget = buildMessage(message, messages, messages.length - 1);
|
||||
}
|
||||
return messageWidget;
|
||||
}
|
||||
|
||||
Widget _buildBottomMessage(
|
||||
BuildContext context,
|
||||
Message message,
|
||||
List<Message> messages,
|
||||
StreamChannelState streamChannel,
|
||||
) {
|
||||
Widget messageWidget;
|
||||
if (widget.messageBuilder != null) {
|
||||
messageWidget = Builder(
|
||||
key: ValueKey<String>('BOTTOM-MESSAGE'),
|
||||
builder: (_) => widget.messageBuilder(
|
||||
context,
|
||||
MessageDetails(
|
||||
context,
|
||||
message,
|
||||
messages,
|
||||
0,
|
||||
),
|
||||
messages,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
messageWidget = buildMessage(message, messages, 0);
|
||||
}
|
||||
|
||||
return VisibilityDetector(
|
||||
key: ValueKey<String>('BOTTOM-MESSAGE'),
|
||||
onVisibilityChanged: (visibility) {
|
||||
final isVisible = visibility.visibleBounds != Rect.zero;
|
||||
if (isVisible && !_bottomWasVisible) {
|
||||
final channel = streamChannel.channel;
|
||||
if (_upToDate &&
|
||||
channel.config?.readEvents == true &&
|
||||
channel.state.unreadCount > 0) {
|
||||
streamChannel.channel.markRead();
|
||||
}
|
||||
_bottomWasVisible = !isVisible;
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() => _showScrollToBottom = !isVisible);
|
||||
}
|
||||
},
|
||||
child: messageWidget,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildParentMessage(
|
||||
Message message,
|
||||
) {
|
||||
final isMyMessage = message.user.id == StreamChat.of(context).user.id;
|
||||
|
||||
return MessageWidget(
|
||||
showThreadReplyIndicator: false,
|
||||
showInChannelIndicator: false,
|
||||
showReplyIndicator: false,
|
||||
message: message,
|
||||
reverse: isMyMessage,
|
||||
showUsername: !isMyMessage,
|
||||
padding: EdgeInsets.only(
|
||||
top: 8.0,
|
||||
left: 8.0,
|
||||
right: 8.0,
|
||||
bottom: 16.0,
|
||||
),
|
||||
showSendingIndicator: DisplayWidget.hide,
|
||||
onThreadTap: _onThreadTap,
|
||||
showEditMessage: false,
|
||||
showDeleteMessage: false,
|
||||
borderRadiusGeometry: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(2),
|
||||
topRight: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
borderSide: isMyMessage ? BorderSide.none : null,
|
||||
showUserAvatar: isMyMessage ? DisplayWidget.gone : DisplayWidget.show,
|
||||
messageTheme: isMyMessage
|
||||
? StreamChatTheme.of(context).ownMessageTheme
|
||||
: StreamChatTheme.of(context).otherMessageTheme,
|
||||
onShowMessage: widget.onShowMessage,
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildMessage(
|
||||
Message message,
|
||||
List<Message> messages,
|
||||
int index,
|
||||
) {
|
||||
if (message.type == 'system' && message.text?.isNotEmpty == true) {
|
||||
return SystemMessage(
|
||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||
message: message,
|
||||
);
|
||||
}
|
||||
|
||||
final userId = StreamChat.of(context).user.id;
|
||||
final isMyMessage = message.user.id == userId;
|
||||
final isNextUser =
|
||||
index - 2 >= 0 && message.user.id == messages[index - 2]?.user?.id;
|
||||
|
||||
final channel = streamChannel.channel;
|
||||
final readList = channel.state?.read
|
||||
?.where((element) => element.user.id != userId)
|
||||
?.where((read) =>
|
||||
(read.lastRead.isAfter(message.createdAt) ||
|
||||
read.lastRead.isAtSameMomentAs(message.createdAt)) &&
|
||||
(index == 0 ||
|
||||
read.lastRead.isBefore(messages[index - 1].createdAt)))
|
||||
?.toList() ??
|
||||
[];
|
||||
|
||||
final allRead = readList.length >= (channel.memberCount ?? 0) - 1;
|
||||
|
||||
final isThreadMessage =
|
||||
widget.parentMessage != null || message?.showInChannel == true;
|
||||
|
||||
Widget child = MessageWidget(
|
||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||
message: message,
|
||||
reverse: isMyMessage,
|
||||
showReactions: !message.isDeleted,
|
||||
padding: EdgeInsets.only(
|
||||
left: 8.0,
|
||||
right: 8.0,
|
||||
bottom: index == 0 ? 30 : (isNextUser ? 2 : 7),
|
||||
top: 3,
|
||||
),
|
||||
onQuotedMessageTap: (quotedMessageId) async {
|
||||
final scrollToIndex = () {
|
||||
final index = messages.indexWhere((m) => m.id == quotedMessageId);
|
||||
_scrollController?.scrollTo(
|
||||
index: index,
|
||||
duration: const Duration(milliseconds: 350),
|
||||
);
|
||||
};
|
||||
if (messages.map((e) => e.id).contains(quotedMessageId)) {
|
||||
scrollToIndex();
|
||||
} else {
|
||||
streamChannel.loadChannelAtMessage(quotedMessageId).then((_) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (messages.map((e) => e.id).contains(quotedMessageId)) {
|
||||
scrollToIndex();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
showInChannelIndicator: widget.parentMessage == null,
|
||||
showThreadReplyIndicator: widget.parentMessage == null,
|
||||
showUsername: !isMyMessage && !isNextUser,
|
||||
showSendingIndicator: isMyMessage &&
|
||||
(index == 0 || message.status != MessageSendingStatus.SENT)
|
||||
? DisplayWidget.show
|
||||
: DisplayWidget.hide,
|
||||
showTimestamp: !isNextUser || readList?.isNotEmpty == true,
|
||||
showEditMessage: isMyMessage,
|
||||
showDeleteMessage: isMyMessage,
|
||||
borderSide: isMyMessage ? BorderSide.none : null,
|
||||
onThreadTap: _onThreadTap,
|
||||
onReplyTap: widget.onReplyTap,
|
||||
attachmentBorderRadiusGeometry: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(!isNextUser ? 0 : 16),
|
||||
topRight: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
attachmentPadding: const EdgeInsets.all(2),
|
||||
borderRadiusGeometry: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
bottomLeft: Radius.circular(!isNextUser ? 0 : 16),
|
||||
topRight: Radius.circular(16),
|
||||
bottomRight: Radius.circular(16),
|
||||
),
|
||||
showUserAvatar: isMyMessage
|
||||
? DisplayWidget.gone
|
||||
: (isNextUser ? DisplayWidget.hide : DisplayWidget.show),
|
||||
messageTheme: isMyMessage
|
||||
? StreamChatTheme.of(context).ownMessageTheme
|
||||
: StreamChatTheme.of(context).otherMessageTheme,
|
||||
readList: readList,
|
||||
allRead: allRead,
|
||||
onShowMessage: widget.onShowMessage,
|
||||
);
|
||||
|
||||
if (!isThreadMessage) {
|
||||
child = Swipeable(
|
||||
onSwipeEnd: () => widget.onMessageSwiped(message),
|
||||
backgroundIcon: StreamSvgIcon.reply(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
if (!initialMessageHighlightComplete &&
|
||||
widget.highlightInitialMessage &&
|
||||
_isInitialMessage(message.id)) {
|
||||
final colorTheme = StreamChatTheme.of(context).colorTheme;
|
||||
final highlightColor =
|
||||
widget.messageHighlightColor ?? colorTheme.highlight;
|
||||
child = TweenAnimationBuilder<Color>(
|
||||
tween: ColorTween(
|
||||
begin: highlightColor,
|
||||
end: colorTheme.white.withOpacity(0),
|
||||
),
|
||||
duration: const Duration(seconds: 3),
|
||||
child: child,
|
||||
onEnd: () => initialMessageHighlightComplete = true,
|
||||
builder: (_, color, child) {
|
||||
return Container(
|
||||
color: color,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
StreamSubscription _messageNewListener;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_scrollController = widget.scrollController ?? ItemScrollController();
|
||||
_itemPositionListener =
|
||||
widget.itemPositionListener ?? ItemPositionsListener.create();
|
||||
|
||||
streamChannel = StreamChannel.of(context);
|
||||
|
||||
initialIndex = _initialIndex;
|
||||
initialAlignment = _initialAlignment;
|
||||
|
||||
_messageNewListener =
|
||||
streamChannel.channel.on(EventType.messageNew).listen((event) {
|
||||
if (_upToDate) {
|
||||
_bottomPaginationActive = false;
|
||||
_topPaginationActive = false;
|
||||
}
|
||||
if (event.message.user.id == streamChannel.channel.client.state.user.id) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_scrollController?.jumpTo(
|
||||
index: 0,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (widget.parentMessage != null) {
|
||||
streamChannel.getReplies(widget.parentMessage.id);
|
||||
}
|
||||
|
||||
_getOnThreadTap();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _getOnThreadTap() {
|
||||
if (widget.onThreadTap != null) {
|
||||
_onThreadTap = (Message message) {
|
||||
widget.onThreadTap(
|
||||
message,
|
||||
widget.threadBuilder != null
|
||||
? widget.threadBuilder(context, message)
|
||||
: null);
|
||||
};
|
||||
} else if (widget.threadBuilder != null) {
|
||||
_onThreadTap = (Message message) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) {
|
||||
return StreamBuilder<Message>(
|
||||
stream: streamChannel.channel.state.messagesStream.map(
|
||||
(messages) =>
|
||||
messages.firstWhere((m) => m.id == message.id)),
|
||||
initialData: message,
|
||||
builder: (_, snapshot) {
|
||||
return StreamChannel(
|
||||
channel: streamChannel.channel,
|
||||
child: widget.threadBuilder(context, snapshot.data),
|
||||
);
|
||||
});
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (!_upToDate) {
|
||||
streamChannel.reloadChannel();
|
||||
}
|
||||
_messageNewListener?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,256 +0,0 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/reaction_bubble.dart';
|
||||
import 'package:stream_chat_flutter/src/reaction_picker.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/user_avatar.dart';
|
||||
|
||||
import 'message_widget.dart';
|
||||
import 'stream_chat_theme.dart';
|
||||
|
||||
class MessageReactionsModal extends StatelessWidget {
|
||||
final Widget Function(BuildContext, Message) editMessageInputBuilder;
|
||||
final void Function(Message) onThreadTap;
|
||||
final Message message;
|
||||
final MessageTheme messageTheme;
|
||||
final bool reverse;
|
||||
final bool showReactions;
|
||||
final DisplayWidget showUserAvatar;
|
||||
final ShapeBorder messageShape;
|
||||
final void Function(User) onUserAvatarTap;
|
||||
|
||||
const MessageReactionsModal({
|
||||
Key key,
|
||||
@required this.message,
|
||||
@required this.messageTheme,
|
||||
this.showReactions = true,
|
||||
this.onThreadTap,
|
||||
this.editMessageInputBuilder,
|
||||
this.messageShape,
|
||||
this.reverse = false,
|
||||
this.showUserAvatar = DisplayWidget.show,
|
||||
this.onUserAvatarTap,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final user = StreamChat.of(context).user;
|
||||
|
||||
final roughMaxSize = 2 * size.width / 3;
|
||||
var messageTextLength = message.text.length;
|
||||
if (message.quotedMessage != null) {
|
||||
var quotedMessageLength = message.quotedMessage.text.length + 40;
|
||||
if (message.quotedMessage.attachments?.isNotEmpty == true) {
|
||||
quotedMessageLength += 40;
|
||||
}
|
||||
if (quotedMessageLength > messageTextLength) {
|
||||
messageTextLength = quotedMessageLength;
|
||||
}
|
||||
}
|
||||
final roughSentenceSize =
|
||||
messageTextLength * messageTheme.messageText.fontSize * 1.2;
|
||||
final divFactor = message.attachments?.isNotEmpty == true
|
||||
? 1
|
||||
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => Navigator.maybePop(context),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
sigmaX: 10,
|
||||
sigmaY: 10,
|
||||
),
|
||||
child: Container(
|
||||
color: StreamChatTheme.of(context).colorTheme.overlay,
|
||||
),
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
if (showReactions &&
|
||||
(message.status == MessageSendingStatus.SENT ||
|
||||
message.status == null))
|
||||
Align(
|
||||
alignment: Alignment(
|
||||
user.id == message.user.id
|
||||
? (divFactor > 1.0 ? 0.0 : (1.0 - divFactor))
|
||||
: (divFactor > 1.0 ? 0.0 : -(1.0 - divFactor)),
|
||||
0.0),
|
||||
child: ReactionPicker(
|
||||
message: message,
|
||||
messageTheme: messageTheme,
|
||||
),
|
||||
),
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
duration: Duration(milliseconds: 300),
|
||||
builder: (context, val, snapshot) {
|
||||
return Transform.scale(
|
||||
scale: val,
|
||||
child: IgnorePointer(
|
||||
child: MessageWidget(
|
||||
key: Key('MessageWidget'),
|
||||
reverse: reverse,
|
||||
message: message.copyWith(
|
||||
text: message.text.length > 200
|
||||
? '${message.text.substring(0, 200)}...'
|
||||
: message.text,
|
||||
),
|
||||
messageTheme: messageTheme,
|
||||
showReactions: false,
|
||||
showUsername: false,
|
||||
showUserAvatar: showUserAvatar,
|
||||
showThreadReplyIndicator: false,
|
||||
showTimestamp: false,
|
||||
translateUserAvatar: false,
|
||||
showSendingIndicator: DisplayWidget.gone,
|
||||
shape: messageShape,
|
||||
showInChannelIndicator: false,
|
||||
showReactionPickerIndicator: showReactions &&
|
||||
(message.status ==
|
||||
MessageSendingStatus.SENT ||
|
||||
message.status == null),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
SizedBox(height: 8),
|
||||
if (message.latestReactions?.isNotEmpty == true)
|
||||
_buildReactionCard(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Padding _buildReactionCard(BuildContext context) {
|
||||
final currentUser = StreamChat.of(context).user;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8.0,
|
||||
),
|
||||
child: Card(
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
'Message Reactions',
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 18,
|
||||
right: 18,
|
||||
bottom: 26,
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 22,
|
||||
alignment: WrapAlignment.start,
|
||||
children: message.latestReactions
|
||||
.map((e) => _buildReaction(
|
||||
e,
|
||||
currentUser,
|
||||
context,
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReaction(
|
||||
Reaction reaction,
|
||||
User currentUser,
|
||||
BuildContext context,
|
||||
) {
|
||||
final isCurrentUser = reaction.user.id == currentUser.id;
|
||||
return TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
duration: Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
builder: (context, val, snapshot) {
|
||||
return Transform.scale(
|
||||
scale: val,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints.loose(Size(
|
||||
64,
|
||||
98,
|
||||
)),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
UserAvatar(
|
||||
onTap: onUserAvatarTap,
|
||||
user: reaction.user,
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 64,
|
||||
width: 64,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
),
|
||||
Positioned(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ReactionBubble(
|
||||
reactions: [reaction],
|
||||
borderColor: messageTheme.reactionsBorderColor,
|
||||
backgroundColor:
|
||||
messageTheme.reactionsBackgroundColor,
|
||||
highlightOwnReactions: false,
|
||||
),
|
||||
),
|
||||
bottom: 4,
|
||||
left: isCurrentUser ? 0 : null,
|
||||
right: isCurrentUser ? 0 : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
reaction.user.name,
|
||||
style: Theme.of(context).textTheme.subtitle2,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
|
||||
import 'stream_chat.dart';
|
||||
|
||||
/// Widget dedicated to the management of a message list with pagination
|
||||
class MessageSearchBloc extends StatefulWidget {
|
||||
/// The widget child
|
||||
final Widget child;
|
||||
|
||||
/// Instantiate a new MessageSearchBloc
|
||||
const MessageSearchBloc({
|
||||
Key key,
|
||||
@required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
MessageSearchBlocState createState() => MessageSearchBlocState();
|
||||
|
||||
/// Use this method to get the current [MessageSearchBlocState] instance
|
||||
static MessageSearchBlocState of(BuildContext context) {
|
||||
MessageSearchBlocState state;
|
||||
|
||||
state = context.findAncestorStateOfType<MessageSearchBlocState>();
|
||||
|
||||
if (state == null) {
|
||||
throw Exception('You must have a MessageSearchBloc widget as ancestor');
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
/// The current state of the [MessageSearchBloc]
|
||||
class MessageSearchBlocState extends State<MessageSearchBloc>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
/// The current messages list
|
||||
List<GetMessageResponse> get messageResponses => _messageResponses.value;
|
||||
|
||||
/// The current messages list as a stream
|
||||
Stream<List<GetMessageResponse>> get messagesStream =>
|
||||
_messageResponses.stream;
|
||||
|
||||
final BehaviorSubject<List<GetMessageResponse>> _messageResponses =
|
||||
BehaviorSubject();
|
||||
|
||||
final BehaviorSubject<bool> _queryMessagesLoadingController =
|
||||
BehaviorSubject.seeded(false);
|
||||
|
||||
/// The stream notifying the state of queryUsers call
|
||||
Stream<bool> get queryMessagesLoading =>
|
||||
_queryMessagesLoadingController.stream;
|
||||
|
||||
/// Calls [Client.search] updating [queryMessagesLoading] stream
|
||||
Future<void> search({
|
||||
Map<String, dynamic> filter,
|
||||
Map<String, dynamic> messageFilter,
|
||||
List<SortOption> sort,
|
||||
String query,
|
||||
PaginationParams pagination,
|
||||
}) async {
|
||||
final client = StreamChat.of(context).client;
|
||||
|
||||
if (client.state?.user == null ||
|
||||
_queryMessagesLoadingController.value == true) {
|
||||
return;
|
||||
}
|
||||
_queryMessagesLoadingController.add(true);
|
||||
try {
|
||||
final clear = pagination == null ||
|
||||
pagination.offset == null ||
|
||||
pagination.offset == 0;
|
||||
|
||||
final oldMessages = List<GetMessageResponse>.from(messageResponses ?? []);
|
||||
|
||||
final messageResponse = await client.search(
|
||||
filter,
|
||||
sort,
|
||||
query,
|
||||
pagination,
|
||||
messageFilters: messageFilter,
|
||||
);
|
||||
|
||||
if (clear) {
|
||||
_messageResponses.add(messageResponse.results);
|
||||
} else {
|
||||
final temp = oldMessages + messageResponse.results;
|
||||
_messageResponses.add(temp);
|
||||
}
|
||||
|
||||
_queryMessagesLoadingController.add(false);
|
||||
} catch (err, stackTrace) {
|
||||
_queryMessagesLoadingController.addError(err, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return widget.child;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_messageResponses.close();
|
||||
_queryMessagesLoadingController.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// It shows the current [Message] preview.
|
||||
///
|
||||
/// Usually you don't use this widget as it's the default item used by [MessageSearchListView].
|
||||
///
|
||||
/// The widget renders the ui based on the first ancestor of type [StreamChatTheme].
|
||||
/// Modify it to change the widget appearance.
|
||||
class MessageSearchItem extends StatelessWidget {
|
||||
/// Instantiate a new MessageSearchItem
|
||||
const MessageSearchItem({
|
||||
Key key,
|
||||
@required this.getMessageResponse,
|
||||
this.onTap,
|
||||
this.showOnlineStatus = true,
|
||||
}) : super(key: key);
|
||||
|
||||
/// [Message] displayed
|
||||
final GetMessageResponse getMessageResponse;
|
||||
|
||||
/// Function called when tapping this widget
|
||||
final VoidCallback onTap;
|
||||
|
||||
/// If true the [MessageSearchItem] will show the current online Status
|
||||
final bool showOnlineStatus;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final message = getMessageResponse.message;
|
||||
final channel = getMessageResponse.channel;
|
||||
final channelName = channel.extraData['name'];
|
||||
final user = message.user;
|
||||
return ListTile(
|
||||
onTap: onTap,
|
||||
leading: UserAvatar(
|
||||
user: user,
|
||||
showOnlineStatus: showOnlineStatus,
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 40,
|
||||
width: 40,
|
||||
),
|
||||
),
|
||||
title: Row(
|
||||
children: [
|
||||
Text(
|
||||
user.id == StreamChat.of(context).user.id ? 'You' : user.name,
|
||||
style: StreamChatTheme.of(context).channelPreviewTheme.title,
|
||||
),
|
||||
if (channelName != null) ...[
|
||||
Text(
|
||||
' in ',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.title
|
||||
.copyWith(
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
channelName,
|
||||
style: StreamChatTheme.of(context).channelPreviewTheme.title,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
subtitle: Row(
|
||||
children: [
|
||||
Expanded(child: _buildSubtitle(context, message)),
|
||||
SizedBox(width: 16),
|
||||
_buildDate(context, message),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDate(BuildContext context, Message message) {
|
||||
final createdAt = message.createdAt;
|
||||
String stringDate;
|
||||
final now = DateTime.now();
|
||||
|
||||
if (now.year != createdAt.year ||
|
||||
now.month != createdAt.month ||
|
||||
now.day != createdAt.day) {
|
||||
stringDate = Jiffy(createdAt.toLocal()).format('dd/MM/yyyy');
|
||||
} else {
|
||||
stringDate = Jiffy(createdAt.toLocal()).format('HH:mm');
|
||||
}
|
||||
|
||||
return Text(
|
||||
stringDate,
|
||||
style: StreamChatTheme.of(context).channelPreviewTheme.lastMessageAt,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubtitle(BuildContext context, Message message) {
|
||||
if (message == null) {
|
||||
return SizedBox();
|
||||
}
|
||||
|
||||
var text = message.text;
|
||||
if (message.isDeleted) {
|
||||
text = 'This message was deleted.';
|
||||
} else if (message.attachments != null) {
|
||||
final parts = <String>[
|
||||
...message.attachments.map((e) {
|
||||
if (e.type == 'image') {
|
||||
return '📷';
|
||||
} else if (e.type == 'video') {
|
||||
return '🎬';
|
||||
} else if (e.type == 'giphy') {
|
||||
return '[GIF]';
|
||||
}
|
||||
return e == message.attachments.last
|
||||
? (e.title ?? 'File')
|
||||
: '${e.title ?? 'File'} , ';
|
||||
}).where((e) => e != null),
|
||||
message.text ?? '',
|
||||
];
|
||||
|
||||
text = parts.join(' ');
|
||||
}
|
||||
|
||||
return Text.rich(
|
||||
_getDisplayText(
|
||||
text,
|
||||
message.mentionedUsers,
|
||||
message.attachments,
|
||||
StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
|
||||
fontStyle: (message.isSystem || message.isDeleted)
|
||||
? FontStyle.italic
|
||||
: FontStyle.normal,
|
||||
),
|
||||
StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
|
||||
fontStyle: (message.isSystem || message.isDeleted)
|
||||
? FontStyle.italic
|
||||
: FontStyle.normal,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
}
|
||||
|
||||
TextSpan _getDisplayText(
|
||||
String text,
|
||||
List<User> mentions,
|
||||
List<Attachment> attachments,
|
||||
TextStyle normalTextStyle,
|
||||
TextStyle mentionsTextStyle) {
|
||||
var textList = text.split(' ');
|
||||
List<TextSpan> resList = [];
|
||||
for (var e in textList) {
|
||||
if (mentions != null &&
|
||||
mentions.isNotEmpty &&
|
||||
mentions.any((element) => '@${element.name}' == e)) {
|
||||
resList.add(TextSpan(
|
||||
text: '$e ',
|
||||
style: mentionsTextStyle,
|
||||
));
|
||||
} else if (attachments != null &&
|
||||
attachments.isNotEmpty &&
|
||||
attachments
|
||||
.where((e) => e.title != null)
|
||||
.any((element) => element.title == e)) {
|
||||
resList.add(TextSpan(
|
||||
text: '$e ',
|
||||
style: normalTextStyle.copyWith(fontStyle: FontStyle.italic),
|
||||
));
|
||||
} else {
|
||||
resList.add(TextSpan(
|
||||
text: e == textList.last ? '$e' : '$e ',
|
||||
style: normalTextStyle,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return TextSpan(children: resList);
|
||||
}
|
||||
}
|
||||
@@ -1,357 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/message_search_item.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
import 'lazy_load_scroll_view.dart';
|
||||
import 'message_search_bloc.dart';
|
||||
|
||||
/// Callback called when tapping on a user
|
||||
typedef MessageSearchItemTapCallback = void Function(GetMessageResponse);
|
||||
|
||||
/// Builder used to create a custom [ListUserItem] from a [User]
|
||||
typedef MessageSearchItemBuilder = Widget Function(
|
||||
BuildContext, GetMessageResponse);
|
||||
|
||||
/// Builder used when [MessageSearchListView] is empty
|
||||
typedef EmptyMessageSearchBuilder = Widget Function(
|
||||
BuildContext context, String searchQuery);
|
||||
|
||||
///
|
||||
/// It shows the list of searched messages.
|
||||
///
|
||||
/// ```dart
|
||||
/// class MessageSearchPage extends StatelessWidget {
|
||||
/// @override
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return Scaffold(
|
||||
/// body: MessageSearchListView(
|
||||
/// messageQuery: _channelQuery,
|
||||
/// filters: {
|
||||
/// 'members': {
|
||||
/// r'$in': [user.id]
|
||||
/// }
|
||||
/// },
|
||||
/// paginationParams: PaginationParams(limit: 20),
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
/// Make sure to have a [MessageSearchBloc] ancestor in order to provide the information about the messages.
|
||||
/// The widget uses a [ListView.separated] to render the list of messages.
|
||||
///
|
||||
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme].
|
||||
/// Modify it to change the widget appearance.
|
||||
class MessageSearchListView extends StatefulWidget {
|
||||
/// Instantiate a new MessageSearchListView
|
||||
const MessageSearchListView({
|
||||
Key key,
|
||||
@required this.messageQuery,
|
||||
@required this.filters,
|
||||
this.sortOptions,
|
||||
this.paginationParams,
|
||||
this.emptyBuilder,
|
||||
this.errorBuilder,
|
||||
this.separatorBuilder,
|
||||
this.itemBuilder,
|
||||
this.onItemTap,
|
||||
this.showResultCount = true,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Message String to search on
|
||||
final String messageQuery;
|
||||
|
||||
/// The query filters to use.
|
||||
/// You can query on any of the custom fields you've defined on the [Channel].
|
||||
/// You can also filter other built-in channel fields.
|
||||
final Map<String, dynamic> filters;
|
||||
|
||||
/// The sorting used for the channels matching the filters.
|
||||
/// Sorting is based on field and direction, multiple sorting options can be provided.
|
||||
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
|
||||
/// Direction can be ascending or descending.
|
||||
final List<SortOption> sortOptions;
|
||||
|
||||
/// Pagination parameters
|
||||
/// limit: the number of users to return (max is 30)
|
||||
/// offset: the offset (max is 1000)
|
||||
/// message_limit: how many messages should be included to each channel
|
||||
final PaginationParams paginationParams;
|
||||
|
||||
/// Builder used to create a custom item preview
|
||||
final MessageSearchItemBuilder itemBuilder;
|
||||
|
||||
/// Function called when tapping on a [MessageSearchItem]
|
||||
final MessageSearchItemTapCallback onItemTap;
|
||||
|
||||
/// The builder used when the channel list is empty.
|
||||
final EmptyMessageSearchBuilder emptyBuilder;
|
||||
|
||||
/// The builder that will be used in case of error
|
||||
final Widget Function(Error error) errorBuilder;
|
||||
|
||||
/// Builder used to create a custom item separator
|
||||
final IndexedWidgetBuilder separatorBuilder;
|
||||
|
||||
/// Set it to false to hide total results text
|
||||
final bool showResultCount;
|
||||
|
||||
@override
|
||||
_MessageSearchListViewState createState() => _MessageSearchListViewState();
|
||||
}
|
||||
|
||||
class _MessageSearchListViewState extends State<MessageSearchListView> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final messageSearchBloc = MessageSearchBloc.of(context);
|
||||
messageSearchBloc.search(
|
||||
filter: widget.filters,
|
||||
sort: widget.sortOptions,
|
||||
query: widget.messageQuery,
|
||||
pagination: widget.paginationParams,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final messageSearchBloc = MessageSearchBloc.of(context);
|
||||
return _buildListView(messageSearchBloc);
|
||||
}
|
||||
|
||||
Widget _separatorBuilder(BuildContext context, int index) {
|
||||
return Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? StreamChatTheme.of(context).colorTheme.white.withOpacity(0.1)
|
||||
: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.1),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _listItemBuilder(
|
||||
BuildContext context, GetMessageResponse getMessageResponse) {
|
||||
if (widget.itemBuilder != null) {
|
||||
return widget.itemBuilder(context, getMessageResponse);
|
||||
}
|
||||
return MessageSearchItem(
|
||||
getMessageResponse: getMessageResponse,
|
||||
onTap: () => widget.onItemTap(getMessageResponse),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQueryProgressIndicator(
|
||||
context, MessageSearchBlocState messageSearchBloc) {
|
||||
return StreamBuilder<bool>(
|
||||
stream: messageSearchBloc.queryMessagesLoading,
|
||||
initialData: false,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Container(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentRed
|
||||
.withOpacity(.2),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: Center(
|
||||
child: Text('Error loading messages'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
height: 100,
|
||||
padding: EdgeInsets.all(32),
|
||||
child: Center(
|
||||
child: snapshot.data ? CircularProgressIndicator() : Container(),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildListView(MessageSearchBlocState messageSearchBloc) {
|
||||
return StreamBuilder<List<GetMessageResponse>>(
|
||||
stream: messageSearchBloc.messagesStream,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
if (snapshot.error is Error) {
|
||||
print((snapshot.error as Error).stackTrace);
|
||||
}
|
||||
|
||||
if (widget.errorBuilder != null) {
|
||||
return widget.errorBuilder(snapshot.error);
|
||||
}
|
||||
|
||||
var message = snapshot.error.toString();
|
||||
if (snapshot.error is DioError) {
|
||||
final dioError = snapshot.error as DioError;
|
||||
if (dioError.type == DioErrorType.RESPONSE) {
|
||||
message = dioError.message;
|
||||
} else {
|
||||
message = 'Check your connection and retry';
|
||||
}
|
||||
}
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
WidgetSpan(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 2.0,
|
||||
),
|
||||
child: Icon(Icons.error_outline),
|
||||
),
|
||||
),
|
||||
TextSpan(text: 'Error loading messages'),
|
||||
],
|
||||
),
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 16.0,
|
||||
),
|
||||
child: Text(message),
|
||||
),
|
||||
FlatButton(
|
||||
onPressed: () {
|
||||
messageSearchBloc.search(
|
||||
filter: widget.filters,
|
||||
sort: widget.sortOptions,
|
||||
query: widget.messageQuery,
|
||||
pagination: widget.paginationParams,
|
||||
);
|
||||
},
|
||||
child: Text('Retry'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!snapshot.hasData) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, viewportConstraints) {
|
||||
return SingleChildScrollView(
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: viewportConstraints.maxHeight,
|
||||
),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final items = snapshot.data;
|
||||
|
||||
if (items.isEmpty) {
|
||||
if (widget.emptyBuilder != null) {
|
||||
return widget.emptyBuilder(context, widget.messageQuery);
|
||||
}
|
||||
return LayoutBuilder(
|
||||
builder: (context, viewportConstraints) {
|
||||
return SingleChildScrollView(
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: viewportConstraints.maxHeight,
|
||||
),
|
||||
child: Center(
|
||||
child: Text('There are no messages currently'),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget child;
|
||||
child = LazyLoadScrollView(
|
||||
onEndOfPage: () => messageSearchBloc.search(
|
||||
filter: widget.filters,
|
||||
sort: widget.sortOptions,
|
||||
pagination: widget.paginationParams.copyWith(
|
||||
offset: messageSearchBloc.messageResponses?.length ?? 0,
|
||||
),
|
||||
query: widget.messageQuery,
|
||||
),
|
||||
child: ListView.separated(
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
itemCount: items.isNotEmpty ? items.length + 1 : items.length,
|
||||
separatorBuilder: (_, index) {
|
||||
if (widget.separatorBuilder != null) {
|
||||
return widget.separatorBuilder(context, index);
|
||||
}
|
||||
return _separatorBuilder(context, index);
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
if (index < items.length) {
|
||||
return _listItemBuilder(context, items[index]);
|
||||
}
|
||||
return _buildQueryProgressIndicator(context, messageSearchBloc);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.showResultCount) {
|
||||
child = Column(
|
||||
children: [
|
||||
Container(
|
||||
width: double.maxFinite,
|
||||
decoration: BoxDecoration(
|
||||
gradient: StreamChatTheme.of(context).colorTheme.bgGradient,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 8,
|
||||
),
|
||||
child: Text(
|
||||
'${items.length} results',
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(child: child),
|
||||
],
|
||||
);
|
||||
}
|
||||
return child;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(MessageSearchListView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.filters?.toString() != oldWidget.filters?.toString() ||
|
||||
jsonEncode(widget.sortOptions) != jsonEncode(oldWidget.sortOptions) ||
|
||||
widget.paginationParams?.toJson()?.toString() !=
|
||||
oldWidget.paginationParams?.toJson()?.toString() ||
|
||||
widget.messageQuery?.toString() != oldWidget.messageQuery?.toString()) {
|
||||
final messageSearchBloc = MessageSearchBloc.of(context);
|
||||
messageSearchBloc.search(
|
||||
filter: widget.filters,
|
||||
sort: widget.sortOptions,
|
||||
query: widget.messageQuery,
|
||||
pagination: widget.paginationParams,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
|
||||
import 'stream_chat_theme.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
class MessageText extends StatelessWidget {
|
||||
const MessageText({
|
||||
Key key,
|
||||
@required this.message,
|
||||
@required this.messageTheme,
|
||||
this.onMentionTap,
|
||||
this.onLinkTap,
|
||||
}) : super(key: key);
|
||||
|
||||
final Message message;
|
||||
final void Function(User) onMentionTap;
|
||||
final void Function(String) onLinkTap;
|
||||
final MessageTheme messageTheme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final text = _replaceMentions(message.text);
|
||||
return MarkdownBody(
|
||||
data: text,
|
||||
onTapLink: (
|
||||
String link,
|
||||
String href,
|
||||
String title,
|
||||
) {
|
||||
if (link.startsWith('@')) {
|
||||
final mentionedUser = message.mentionedUsers.firstWhere(
|
||||
(u) => '@${u.name}' == link,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
if (onMentionTap != null) {
|
||||
onMentionTap(mentionedUser);
|
||||
} else {
|
||||
print('tap on ${mentionedUser.name}');
|
||||
}
|
||||
} else {
|
||||
if (onLinkTap != null) {
|
||||
onLinkTap(link);
|
||||
} else {
|
||||
launchURL(context, link);
|
||||
}
|
||||
}
|
||||
},
|
||||
styleSheet: MarkdownStyleSheet.fromTheme(
|
||||
Theme.of(context).copyWith(
|
||||
textTheme: Theme.of(context).textTheme.apply(
|
||||
bodyColor: messageTheme.messageText.color,
|
||||
decoration: messageTheme.messageText.decoration,
|
||||
decorationColor: messageTheme.messageText.decorationColor,
|
||||
decorationStyle: messageTheme.messageText.decorationStyle,
|
||||
fontFamily: messageTheme.messageText.fontFamily,
|
||||
),
|
||||
),
|
||||
).copyWith(
|
||||
a: messageTheme.messageLinks,
|
||||
p: messageTheme.messageText,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _replaceMentions(String text) {
|
||||
message.mentionedUsers?.forEach((u) {
|
||||
text = text.replaceAll(
|
||||
'@${u.name}', '[@${u.name}](@${u.name.replaceAll(' ', '')})');
|
||||
});
|
||||
return text;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,81 +0,0 @@
|
||||
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';
|
||||
|
||||
class OptionListTile extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget leading;
|
||||
final Widget trailing;
|
||||
final VoidCallback onTap;
|
||||
final Color titleColor;
|
||||
final Color tileColor;
|
||||
final Color separatorColor;
|
||||
final TextStyle titleTextStyle;
|
||||
|
||||
OptionListTile({
|
||||
this.title,
|
||||
this.leading,
|
||||
this.trailing,
|
||||
this.onTap,
|
||||
this.titleColor,
|
||||
this.tileColor,
|
||||
this.separatorColor,
|
||||
this.titleTextStyle,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
color: separatorColor ??
|
||||
StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
height: 1.0,
|
||||
),
|
||||
Material(
|
||||
color: tileColor ?? StreamChatTheme.of(context).colorTheme.white,
|
||||
child: Container(
|
||||
height: 63.0,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Row(
|
||||
children: [
|
||||
if (leading != null) Center(child: leading),
|
||||
if (leading == null)
|
||||
SizedBox(
|
||||
width: 16.0,
|
||||
),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Text(
|
||||
title,
|
||||
style: titleTextStyle ??
|
||||
(titleColor == null
|
||||
? StreamChatTheme.of(context).textTheme.bodyBold
|
||||
: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.bodyBold
|
||||
.copyWith(
|
||||
color: titleColor,
|
||||
)),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 16.0),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: trailing ?? Container(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,321 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:emojis/emoji.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import 'attachment_error.dart';
|
||||
import 'image_attachment.dart';
|
||||
import 'message_text.dart';
|
||||
import 'stream_chat_theme.dart';
|
||||
import 'user_avatar.dart';
|
||||
import 'utils.dart';
|
||||
import 'extension.dart';
|
||||
|
||||
typedef QuotedMessageAttachmentThumbnailBuilder = Widget Function(
|
||||
BuildContext,
|
||||
Attachment,
|
||||
);
|
||||
|
||||
class _VideoAttachmentThumbnail extends StatefulWidget {
|
||||
final Size size;
|
||||
final Attachment attachment;
|
||||
|
||||
const _VideoAttachmentThumbnail({
|
||||
Key key,
|
||||
@required this.attachment,
|
||||
this.size = const Size(32, 32),
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_VideoAttachmentThumbnailState createState() =>
|
||||
_VideoAttachmentThumbnailState();
|
||||
}
|
||||
|
||||
class _VideoAttachmentThumbnailState extends State<_VideoAttachmentThumbnail> {
|
||||
VideoPlayerController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = VideoPlayerController.network(widget.attachment.assetUrl)
|
||||
..initialize().then((_) {
|
||||
setState(() {}); //when your thumbnail will show.
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_controller.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: widget.size.height,
|
||||
width: widget.size.width,
|
||||
child: _controller.value.initialized
|
||||
? VideoPlayer(_controller)
|
||||
: CircularProgressIndicator());
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
class QuotedMessageWidget extends StatelessWidget {
|
||||
/// The message
|
||||
final Message message;
|
||||
|
||||
/// The message theme
|
||||
final MessageTheme messageTheme;
|
||||
|
||||
/// If true the widget will be mirrored
|
||||
final bool reverse;
|
||||
|
||||
/// If true the message will show a grey border
|
||||
final bool showBorder;
|
||||
|
||||
/// limit of the text message shown
|
||||
final int textLimit;
|
||||
|
||||
/// Map that defines a thumbnail builder for an attachment type
|
||||
final Map<String, QuotedMessageAttachmentThumbnailBuilder>
|
||||
attachmentThumbnailBuilders;
|
||||
|
||||
final GestureTapCallback onTap;
|
||||
|
||||
///
|
||||
QuotedMessageWidget({
|
||||
Key key,
|
||||
@required this.message,
|
||||
@required this.messageTheme,
|
||||
this.reverse = false,
|
||||
this.showBorder = false,
|
||||
this.textLimit = 170,
|
||||
this.attachmentThumbnailBuilders,
|
||||
this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
bool get _hasAttachments => message.attachments?.isNotEmpty == true;
|
||||
|
||||
bool get _containsScrapeUrl =>
|
||||
message.attachments?.any((element) => element.ogScrapeUrl != null) ==
|
||||
true;
|
||||
|
||||
bool get _containsText => message?.text?.isNotEmpty == true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 8, bottom: 6, right: 4, left: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Flexible(child: _buildMessage(context)),
|
||||
SizedBox(width: 4),
|
||||
_buildUserAvatar(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessage(BuildContext context) {
|
||||
final isOnlyEmoji =
|
||||
message.text.characters.every((c) => Emoji.byChar(c) != null);
|
||||
var msg = _hasAttachments && !_containsText
|
||||
? message.copyWith(text: message.attachments.last?.title ?? '')
|
||||
: message;
|
||||
if (msg.text.length > textLimit) {
|
||||
msg = msg.copyWith(text: '${msg.text.substring(0, textLimit - 3)}...');
|
||||
}
|
||||
|
||||
final children = [
|
||||
if (_hasAttachments) _parseAttachments(context),
|
||||
if (msg.text.isNotEmpty)
|
||||
Flexible(
|
||||
child: Transform(
|
||||
transform: Matrix4.rotationY(reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: MessageText(
|
||||
message: msg,
|
||||
messageTheme: isOnlyEmoji && _containsText
|
||||
? messageTheme.copyWith(
|
||||
messageText: messageTheme.messageText.copyWith(
|
||||
fontSize: 24,
|
||||
))
|
||||
: messageTheme,
|
||||
),
|
||||
),
|
||||
),
|
||||
].insertBetween(const SizedBox(width: 8));
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: _getBackgroundColor(context),
|
||||
border: showBorder
|
||||
? Border.all(
|
||||
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
|
||||
)
|
||||
: null,
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(12),
|
||||
topLeft: Radius.circular(12),
|
||||
bottomLeft: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment:
|
||||
reverse ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
children: reverse ? children.reversed.toList() : children,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUrlAttachment(Attachment attachment) {
|
||||
final size = Size(32, 32);
|
||||
if (attachment.thumbUrl != null) {
|
||||
return Container(
|
||||
height: size.height,
|
||||
width: size.width,
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
image: CachedNetworkImageProvider(
|
||||
attachment.imageUrl,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return AttachmentError(
|
||||
attachment: attachment,
|
||||
size: size,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _parseAttachments(BuildContext context) {
|
||||
Widget child;
|
||||
Attachment attachment;
|
||||
if (_containsScrapeUrl) {
|
||||
attachment = message.attachments.firstWhere(
|
||||
(element) => element.ogScrapeUrl != null,
|
||||
);
|
||||
child = _buildUrlAttachment(attachment);
|
||||
} else {
|
||||
QuotedMessageAttachmentThumbnailBuilder attachmentBuilder;
|
||||
attachment = message.attachments.last;
|
||||
if (attachmentThumbnailBuilders?.containsKey(attachment?.type) == true) {
|
||||
attachmentBuilder = attachmentThumbnailBuilders[attachment?.type];
|
||||
}
|
||||
attachmentBuilder = _defaultAttachmentBuilder[attachment?.type];
|
||||
if (attachmentBuilder == null) {
|
||||
child = Offstage();
|
||||
}
|
||||
child = attachmentBuilder(context, attachment);
|
||||
}
|
||||
child = AbsorbPointer(child: child);
|
||||
return Transform(
|
||||
transform: Matrix4.rotationY(reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: Material(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
color: Colors.transparent,
|
||||
shape: attachment.type == 'file' ? null : _getDefaultShape(context),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ShapeBorder _getDefaultShape(BuildContext context) {
|
||||
return RoundedRectangleBorder(
|
||||
side: BorderSide(
|
||||
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUserAvatar() {
|
||||
return Transform(
|
||||
transform: Matrix4.rotationY(reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: UserAvatar(
|
||||
user: message.user,
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
showOnlineStatus: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, QuotedMessageAttachmentThumbnailBuilder>
|
||||
get _defaultAttachmentBuilder {
|
||||
return {
|
||||
'image': (_, attachment) {
|
||||
return ImageAttachment(
|
||||
attachment: attachment,
|
||||
message: message,
|
||||
messageTheme: messageTheme,
|
||||
size: Size(32, 32),
|
||||
);
|
||||
},
|
||||
'video': (_, attachment) {
|
||||
return _VideoAttachmentThumbnail(
|
||||
key: ValueKey(attachment.assetUrl),
|
||||
attachment: attachment,
|
||||
);
|
||||
},
|
||||
'giphy': (_, attachment) {
|
||||
final size = Size(32, 32);
|
||||
return 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,
|
||||
);
|
||||
},
|
||||
'file': (_, attachment) {
|
||||
return Container(
|
||||
height: 32,
|
||||
width: 32,
|
||||
child: getFileTypeImage(attachment.extraData['mime_type']),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Color _getBackgroundColor(BuildContext context) {
|
||||
if (_containsScrapeUrl) {
|
||||
return StreamChatTheme.of(context).colorTheme.blueAlice;
|
||||
}
|
||||
return messageTheme.messageBackgroundColor;
|
||||
}
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:stream_chat_flutter/src/reaction_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
class ReactionBubble extends StatelessWidget {
|
||||
const ReactionBubble({
|
||||
Key key,
|
||||
@required this.reactions,
|
||||
@required this.borderColor,
|
||||
@required this.backgroundColor,
|
||||
this.reverse = false,
|
||||
this.flipTail = false,
|
||||
this.highlightOwnReactions = true,
|
||||
}) : super(key: key);
|
||||
|
||||
final List<Reaction> reactions;
|
||||
final Color borderColor;
|
||||
final Color backgroundColor;
|
||||
final bool reverse;
|
||||
final bool flipTail;
|
||||
final bool highlightOwnReactions;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final reactionIcons = StreamChatTheme.of(context).reactionIcons;
|
||||
final offset = reactions.length > 1 ? 16.0 : 2.0;
|
||||
return Transform(
|
||||
transform: Matrix4.rotationY(reverse ? pi : 0),
|
||||
alignment: Alignment.center,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Transform.translate(
|
||||
offset: Offset(reverse ? offset : -offset, 0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: borderColor,
|
||||
),
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.all(Radius.circular(14)),
|
||||
),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return Flex(
|
||||
direction: Axis.horizontal,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (constraints.maxWidth < double.infinity)
|
||||
...reactions
|
||||
.take((constraints.maxWidth) ~/ 22)
|
||||
.map((reaction) {
|
||||
return _buildReaction(
|
||||
reactionIcons,
|
||||
reaction,
|
||||
context,
|
||||
);
|
||||
}).toList(),
|
||||
if (constraints.maxWidth == double.infinity)
|
||||
...reactions.map((reaction) {
|
||||
return _buildReaction(
|
||||
reactionIcons,
|
||||
reaction,
|
||||
context,
|
||||
);
|
||||
}).toList(),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: reverse ? null : 11,
|
||||
right: !reverse ? null : 11,
|
||||
child: _buildReactionsTail(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReaction(
|
||||
List<ReactionIcon> reactionIcons,
|
||||
Reaction reaction,
|
||||
BuildContext context,
|
||||
) {
|
||||
final reactionIcon = reactionIcons.firstWhere(
|
||||
(r) => r.type == reaction.type,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4.0,
|
||||
),
|
||||
child: reactionIcon != null
|
||||
? StreamSvgIcon(
|
||||
assetName: reactionIcon.assetName,
|
||||
width: 16,
|
||||
height: 16,
|
||||
color: (!highlightOwnReactions ||
|
||||
reaction.user.id == StreamChat.of(context).user.id)
|
||||
? StreamChatTheme.of(context).colorTheme.accentBlue
|
||||
: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5),
|
||||
)
|
||||
: Icon(
|
||||
Icons.help_outline_rounded,
|
||||
size: 16,
|
||||
color: (!highlightOwnReactions ||
|
||||
reaction.user.id == StreamChat.of(context).user.id)
|
||||
? StreamChatTheme.of(context).colorTheme.accentBlue
|
||||
: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(.5),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReactionsTail(BuildContext context) {
|
||||
final tail = CustomPaint(
|
||||
painter: ReactionBubblePainter(
|
||||
backgroundColor,
|
||||
borderColor,
|
||||
),
|
||||
);
|
||||
|
||||
if (!flipTail) {
|
||||
return tail;
|
||||
} else {
|
||||
return Transform(
|
||||
transform: Matrix4.rotationY(pi),
|
||||
alignment: Alignment.center,
|
||||
child: tail,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ReactionBubblePainter extends CustomPainter {
|
||||
final Color color;
|
||||
final Color borderColor;
|
||||
|
||||
ReactionBubblePainter(
|
||||
this.color,
|
||||
this.borderColor,
|
||||
);
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
_drawOval(size, canvas);
|
||||
|
||||
_drawOvalBorder(size, canvas);
|
||||
|
||||
_drawArc(size, canvas);
|
||||
|
||||
_drawBorder(size, canvas);
|
||||
}
|
||||
|
||||
void _drawOvalBorder(Size size, Canvas canvas) {
|
||||
final paint = Paint()
|
||||
..color = borderColor
|
||||
..strokeWidth = 1
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
final path = Path();
|
||||
path.addOval(
|
||||
Rect.fromCircle(
|
||||
center: Offset(4, 3),
|
||||
radius: 2,
|
||||
),
|
||||
);
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
void _drawOval(Size size, Canvas canvas) {
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..strokeWidth = 1;
|
||||
|
||||
final path = Path();
|
||||
path.addOval(Rect.fromCircle(
|
||||
center: Offset(4, 3),
|
||||
radius: 2,
|
||||
));
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
void _drawBorder(Size size, Canvas canvas) {
|
||||
final paint = Paint()
|
||||
..color = borderColor
|
||||
..strokeWidth = 1
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
final dy = -2.2;
|
||||
final startAngle = 1.1;
|
||||
final sweepAngle = 1.2;
|
||||
final path = Path();
|
||||
path.addArc(
|
||||
Rect.fromCircle(
|
||||
center: Offset(1, dy),
|
||||
radius: 4,
|
||||
),
|
||||
-pi * startAngle,
|
||||
-pi / sweepAngle,
|
||||
);
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
void _drawArc(Size size, Canvas canvas) {
|
||||
final paint = Paint()
|
||||
..color = color
|
||||
..strokeWidth = 1;
|
||||
|
||||
final dy = -2.2;
|
||||
final startAngle = 1;
|
||||
final sweepAngle = 1.3;
|
||||
final path = Path();
|
||||
path.addArc(
|
||||
Rect.fromCircle(
|
||||
center: Offset(1, dy),
|
||||
radius: 4,
|
||||
),
|
||||
-pi * startAngle,
|
||||
-pi * sweepAngle,
|
||||
);
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(CustomPainter oldDelegate) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
class ReactionIcon {
|
||||
final String type;
|
||||
final String assetName;
|
||||
|
||||
ReactionIcon({
|
||||
this.type,
|
||||
this.assetName,
|
||||
});
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
import 'package:ezanimation/ezanimation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
|
||||
/// 
|
||||
/// 
|
||||
///
|
||||
/// It shows a reaction picker
|
||||
///
|
||||
/// Usually you don't use this widget as it's one of the default widgets used by [MessageWidget.onMessageActions].
|
||||
|
||||
class ReactionPicker extends StatefulWidget {
|
||||
const ReactionPicker({
|
||||
Key key,
|
||||
@required this.message,
|
||||
@required this.messageTheme,
|
||||
}) : super(key: key);
|
||||
|
||||
final Message message;
|
||||
final MessageTheme messageTheme;
|
||||
|
||||
@override
|
||||
_ReactionPickerState createState() => _ReactionPickerState();
|
||||
}
|
||||
|
||||
class _ReactionPickerState extends State<ReactionPicker>
|
||||
with TickerProviderStateMixin {
|
||||
List<EzAnimation> animations = [];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final reactionIcons = StreamChatTheme.of(context).reactionIcons;
|
||||
|
||||
if (animations.isEmpty && reactionIcons.isNotEmpty) {
|
||||
reactionIcons.forEach((element) {
|
||||
animations.add(
|
||||
EzAnimation.sequence(
|
||||
[
|
||||
SequenceItem(0.0, 1.4),
|
||||
SequenceItem(1.4, 1.0),
|
||||
],
|
||||
Duration(milliseconds: 500),
|
||||
vsync: this,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
triggerAnimations();
|
||||
}
|
||||
|
||||
return TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0.0, end: 1.0),
|
||||
curve: Curves.easeInOutExpo,
|
||||
duration: Duration(milliseconds: 500),
|
||||
builder: (context, val, wid) {
|
||||
return Transform.scale(
|
||||
scale: val,
|
||||
child: Material(
|
||||
color: widget.messageTheme.reactionsBackgroundColor,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: reactionIcons.map((reactionIcon) {
|
||||
final ownReactionIndex = widget.message.ownReactions
|
||||
?.indexWhere((reaction) =>
|
||||
reaction.type == reactionIcon.type) ??
|
||||
-1;
|
||||
var index = reactionIcons.indexOf(reactionIcon);
|
||||
|
||||
return IconButton(
|
||||
iconSize: 24,
|
||||
icon: AnimatedBuilder(
|
||||
animation: animations[index],
|
||||
builder: (context, val) {
|
||||
return Transform(
|
||||
transform: Matrix4.identity()
|
||||
..scale(animations[index].value,
|
||||
animations[index].value)
|
||||
..rotateZ(1.0 - animations[index].value),
|
||||
child: StreamSvgIcon(
|
||||
assetName: reactionIcon.assetName,
|
||||
height: animations[index].value * 24.0,
|
||||
width: animations[index].value * 24.0,
|
||||
color: ownReactionIndex != -1
|
||||
? StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue
|
||||
: Theme.of(context)
|
||||
.iconTheme
|
||||
.color
|
||||
.withOpacity(.5),
|
||||
),
|
||||
);
|
||||
}),
|
||||
onPressed: () {
|
||||
if (ownReactionIndex != -1) {
|
||||
removeReaction(
|
||||
context,
|
||||
widget.message.ownReactions[ownReactionIndex],
|
||||
);
|
||||
} else {
|
||||
sendReaction(
|
||||
context,
|
||||
reactionIcon.type,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void triggerAnimations() async {
|
||||
for (var a in animations) {
|
||||
a.start();
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
}
|
||||
}
|
||||
|
||||
void pop() async {
|
||||
for (var a in animations) {
|
||||
a.stop();
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
/// Add a reaction to the message
|
||||
void sendReaction(BuildContext context, String reactionType) {
|
||||
StreamChannel.of(context).channel.sendReaction(
|
||||
widget.message,
|
||||
reactionType,
|
||||
enforceUnique: true,
|
||||
);
|
||||
pop();
|
||||
}
|
||||
|
||||
/// Remove a reaction from the message
|
||||
void removeReaction(BuildContext context, Reaction reaction) {
|
||||
StreamChannel.of(context).channel.deleteReaction(widget.message, reaction);
|
||||
pop();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var a in animations) {
|
||||
a?.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// Used to show the sending status of the message
|
||||
class SendingIndicator extends StatelessWidget {
|
||||
final Message message;
|
||||
final bool isMessageRead;
|
||||
final double size;
|
||||
|
||||
const SendingIndicator({
|
||||
Key key,
|
||||
this.message,
|
||||
this.isMessageRead = false,
|
||||
this.size = 12,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isMessageRead) {
|
||||
return StreamSvgIcon.checkAll(
|
||||
size: size,
|
||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
);
|
||||
}
|
||||
if (message.status == MessageSendingStatus.SENT || message.status == null) {
|
||||
return StreamSvgIcon.check(
|
||||
size: size,
|
||||
color: IconTheme.of(context).color.withOpacity(0.5),
|
||||
);
|
||||
}
|
||||
if (message.status == MessageSendingStatus.SENDING ||
|
||||
message.status == MessageSendingStatus.UPDATING) {
|
||||
return Icon(
|
||||
Icons.access_time,
|
||||
size: size,
|
||||
);
|
||||
}
|
||||
return SizedBox();
|
||||
}
|
||||
}
|
||||
@@ -1,356 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
|
||||
enum QueryDirection { top, bottom }
|
||||
|
||||
/// Widget used to provide information about the channel to the widget tree
|
||||
///
|
||||
/// Use [StreamChannel.of] to get the current [StreamChannelState] instance.
|
||||
class StreamChannel extends StatefulWidget {
|
||||
const StreamChannel({
|
||||
Key key,
|
||||
@required this.child,
|
||||
@required this.channel,
|
||||
this.showLoading = true,
|
||||
this.initialMessageId,
|
||||
}) : assert(child != null),
|
||||
assert(channel != null),
|
||||
super(key: key);
|
||||
|
||||
final Widget child;
|
||||
final Channel channel;
|
||||
final bool showLoading;
|
||||
|
||||
/// If passed the channel will load from this particular message.
|
||||
final String initialMessageId;
|
||||
|
||||
/// Use this method to get the current [StreamChannelState] instance
|
||||
static StreamChannelState of(BuildContext context) {
|
||||
StreamChannelState streamChannelState;
|
||||
|
||||
streamChannelState = context.findAncestorStateOfType<StreamChannelState>();
|
||||
|
||||
if (streamChannelState == null) {
|
||||
throw Exception(
|
||||
'You must have a StreamChannel widget at the top of your widget tree');
|
||||
}
|
||||
|
||||
return streamChannelState;
|
||||
}
|
||||
|
||||
@override
|
||||
StreamChannelState createState() => StreamChannelState();
|
||||
}
|
||||
|
||||
class StreamChannelState extends State<StreamChannel> {
|
||||
/// Current channel
|
||||
Channel get channel => widget.channel;
|
||||
|
||||
/// InitialMessageId
|
||||
String get initialMessageId => widget.initialMessageId;
|
||||
|
||||
/// Current channel state stream
|
||||
Stream<ChannelState> get channelStateStream =>
|
||||
widget.channel.state.channelStateStream;
|
||||
|
||||
final _queryTopMessagesController = BehaviorSubject.seeded(false);
|
||||
final _queryBottomMessagesController = BehaviorSubject.seeded(false);
|
||||
|
||||
/// The stream notifying the state of [_queryTopMessages] call
|
||||
Stream<bool> get queryTopMessages => _queryTopMessagesController.stream;
|
||||
|
||||
/// The stream notifying the state of [_queryBottomMessages] call
|
||||
Stream<bool> get queryBottomMessages => _queryBottomMessagesController.stream;
|
||||
|
||||
bool _topPaginationEnded = false;
|
||||
bool _bottomPaginationEnded = false;
|
||||
|
||||
Future<void> _queryTopMessages({
|
||||
int limit = 20,
|
||||
bool preferOffline = false,
|
||||
}) async {
|
||||
if (_topPaginationEnded || _queryTopMessagesController?.value == true) {
|
||||
return;
|
||||
}
|
||||
_queryTopMessagesController.add(true);
|
||||
|
||||
if (channel.state.messages.isEmpty) {
|
||||
return _queryTopMessagesController.add(false);
|
||||
}
|
||||
|
||||
final oldestMessage = channel.state.messages.first;
|
||||
|
||||
try {
|
||||
final state = await queryBeforeMessage(
|
||||
oldestMessage.id,
|
||||
limit: limit,
|
||||
preferOffline: preferOffline,
|
||||
);
|
||||
if (state.messages.isEmpty || state.messages.length < limit) {
|
||||
_topPaginationEnded = true;
|
||||
}
|
||||
_queryTopMessagesController.add(false);
|
||||
} catch (e, stk) {
|
||||
_queryTopMessagesController.addError(e, stk);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _queryBottomMessages({
|
||||
int limit = 20,
|
||||
bool preferOffline = false,
|
||||
}) async {
|
||||
if (_bottomPaginationEnded ||
|
||||
_queryBottomMessagesController?.value == true ||
|
||||
channel?.state?.isUpToDate == true) return;
|
||||
_queryBottomMessagesController.add(true);
|
||||
|
||||
if (channel.state.messages.isEmpty) {
|
||||
return _queryBottomMessagesController.add(false);
|
||||
}
|
||||
|
||||
final recentMessage = channel.state.messages.last;
|
||||
|
||||
try {
|
||||
final state = await queryAfterMessage(
|
||||
recentMessage.id,
|
||||
limit: limit,
|
||||
preferOffline: preferOffline,
|
||||
);
|
||||
if (state.messages.isEmpty || state.messages.length < limit) {
|
||||
_bottomPaginationEnded = true;
|
||||
}
|
||||
_queryBottomMessagesController.add(false);
|
||||
} catch (e, stk) {
|
||||
_queryBottomMessagesController.addError(e, stk);
|
||||
}
|
||||
}
|
||||
|
||||
/// Calls [channel.query] updating [queryMessage] stream
|
||||
Future<void> queryMessages({QueryDirection direction = QueryDirection.top}) {
|
||||
if (direction == QueryDirection.top) return _queryTopMessages();
|
||||
return _queryBottomMessages();
|
||||
}
|
||||
|
||||
/// Calls [channel.getReplies] updating [queryMessage] stream
|
||||
Future<void> getReplies(
|
||||
String parentId, {
|
||||
int limit = 50,
|
||||
bool preferOffline = false,
|
||||
}) async {
|
||||
if (_topPaginationEnded || _queryTopMessagesController.value) return;
|
||||
_queryTopMessagesController.add(true);
|
||||
|
||||
Message message;
|
||||
if (channel.state.threads.containsKey(parentId)) {
|
||||
final thread = channel.state.threads[parentId];
|
||||
if (thread.isNotEmpty) {
|
||||
message = thread.first;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await channel.getReplies(
|
||||
parentId,
|
||||
PaginationParams(
|
||||
lessThan: message?.id,
|
||||
limit: limit,
|
||||
),
|
||||
preferOffline: preferOffline,
|
||||
);
|
||||
if (response.messages.isEmpty || response.messages.length < limit) {
|
||||
_topPaginationEnded = true;
|
||||
}
|
||||
_queryTopMessagesController.add(false);
|
||||
} catch (e, stk) {
|
||||
_queryTopMessagesController.addError(e, stk);
|
||||
}
|
||||
}
|
||||
|
||||
/// Query the channel members and watchers
|
||||
Future<void> queryMembersAndWatchers() async {
|
||||
await widget.channel.query(
|
||||
membersPagination: PaginationParams(
|
||||
offset: channel.state.members?.length,
|
||||
limit: 100,
|
||||
),
|
||||
watchersPagination: PaginationParams(
|
||||
offset: channel.state.watchers?.length,
|
||||
limit: 100,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Loads channel at specific message
|
||||
Future<void> loadChannelAtMessage(
|
||||
String messageId, {
|
||||
int before = 20,
|
||||
int after = 20,
|
||||
bool preferOffline = false,
|
||||
}) {
|
||||
return queryAtMessage(
|
||||
messageId: messageId,
|
||||
before: before,
|
||||
after: after,
|
||||
preferOffline: preferOffline,
|
||||
);
|
||||
}
|
||||
|
||||
///
|
||||
Future<void> queryAtMessage({
|
||||
String messageId,
|
||||
int before = 20,
|
||||
int after = 20,
|
||||
bool preferOffline = false,
|
||||
}) async {
|
||||
if (channel.state == null) return;
|
||||
channel.state.isUpToDate = false;
|
||||
channel.state.truncate();
|
||||
|
||||
if (messageId == null) {
|
||||
await channel.query(
|
||||
messagesPagination: PaginationParams(
|
||||
limit: before,
|
||||
),
|
||||
preferOffline: preferOffline,
|
||||
);
|
||||
channel.state.isUpToDate = true;
|
||||
return;
|
||||
}
|
||||
|
||||
return Future.wait([
|
||||
queryBeforeMessage(
|
||||
messageId,
|
||||
limit: before,
|
||||
preferOffline: preferOffline,
|
||||
),
|
||||
queryAfterMessage(
|
||||
messageId,
|
||||
limit: after,
|
||||
preferOffline: preferOffline,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
///
|
||||
Future<ChannelState> queryBeforeMessage(
|
||||
String messageId, {
|
||||
int limit = 20,
|
||||
bool preferOffline = false,
|
||||
}) {
|
||||
return channel.query(
|
||||
messagesPagination: PaginationParams(
|
||||
lessThan: messageId,
|
||||
limit: limit,
|
||||
),
|
||||
preferOffline: preferOffline,
|
||||
);
|
||||
}
|
||||
|
||||
///
|
||||
Future<ChannelState> queryAfterMessage(
|
||||
String messageId, {
|
||||
int limit = 20,
|
||||
bool preferOffline = false,
|
||||
}) async {
|
||||
final state = await channel.query(
|
||||
messagesPagination: PaginationParams(
|
||||
greaterThanOrEqual: messageId,
|
||||
limit: limit,
|
||||
),
|
||||
preferOffline: preferOffline,
|
||||
);
|
||||
if (state.messages.isEmpty || state.messages.length < limit) {
|
||||
channel.state.isUpToDate = true;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
///
|
||||
Future<Message> getMessage(String messageId) async {
|
||||
var message = channel.state.messages.firstWhere(
|
||||
(it) => it.id == messageId,
|
||||
orElse: () => null,
|
||||
);
|
||||
if (message == null) {
|
||||
final response = await channel.getMessagesById([messageId]);
|
||||
message = response.messages.first;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
/// Reloads the channel with latest message
|
||||
Future<void> reloadChannel() => queryAtMessage(before: 30);
|
||||
|
||||
List<Future<bool>> _futures;
|
||||
|
||||
Future<bool> get _loadChannelAtMessage async {
|
||||
try {
|
||||
await loadChannelAtMessage(initialMessageId);
|
||||
return true;
|
||||
} catch (e, stk) {
|
||||
print('Error: $e\nStack: $stk');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_futures = [widget.channel.initialized];
|
||||
if (initialMessageId != null) {
|
||||
_futures.add(_loadChannelAtMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_queryTopMessagesController.close();
|
||||
_queryBottomMessagesController.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child = FutureBuilder<List<bool>>(
|
||||
future: Future.wait(_futures),
|
||||
initialData: [
|
||||
channel.state != null,
|
||||
if (initialMessageId != null) false,
|
||||
],
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
if (snapshot.error is Error) {
|
||||
print((snapshot.error as Error).stackTrace);
|
||||
}
|
||||
var message = snapshot.error.toString();
|
||||
if (snapshot.error is DioError) {
|
||||
final dioError = snapshot.error as DioError;
|
||||
if (dioError.type == DioErrorType.RESPONSE) {
|
||||
message = dioError.message;
|
||||
} else {
|
||||
message = 'Check your connection and retry';
|
||||
}
|
||||
}
|
||||
return Center(
|
||||
child: Text(message),
|
||||
);
|
||||
}
|
||||
final initialized = snapshot.data[0];
|
||||
final dataLoaded = initialMessageId == null ? true : snapshot.data[1];
|
||||
if (widget.showLoading && (!initialized || !dataLoaded)) {
|
||||
return Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
return widget.child;
|
||||
},
|
||||
);
|
||||
if (initialMessageId != null) {
|
||||
child = Material(child: child);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_app_badger/flutter_app_badger.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
|
||||
/// Widget used to provide information about the chat to the widget tree
|
||||
///
|
||||
/// class MyApp extends StatelessWidget {
|
||||
/// final Client client;
|
||||
///
|
||||
/// MyApp(this.client);
|
||||
///
|
||||
/// @override
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return MaterialApp(
|
||||
/// home: Container(
|
||||
/// child: StreamChat(
|
||||
/// client: client,
|
||||
/// child: ChannelListPage(),
|
||||
/// ),
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// Use [StreamChat.of] to get the current [StreamChatState] instance.
|
||||
class StreamChat extends StatefulWidget {
|
||||
final Client client;
|
||||
final Widget child;
|
||||
final StreamChatThemeData streamChatThemeData;
|
||||
|
||||
StreamChat({
|
||||
Key key,
|
||||
@required this.client,
|
||||
@required this.child,
|
||||
this.streamChatThemeData,
|
||||
}) : super(
|
||||
key: key,
|
||||
);
|
||||
|
||||
@override
|
||||
StreamChatState createState() => StreamChatState();
|
||||
|
||||
/// Use this method to get the current [StreamChatState] instance
|
||||
static StreamChatState of(BuildContext context) {
|
||||
StreamChatState streamChatState;
|
||||
|
||||
streamChatState = context.findAncestorStateOfType<StreamChatState>();
|
||||
|
||||
if (streamChatState == null) {
|
||||
throw Exception(
|
||||
'You must have a StreamChat widget at the top of your widget tree');
|
||||
}
|
||||
|
||||
return streamChatState;
|
||||
}
|
||||
}
|
||||
|
||||
/// The current state of the StreamChat widget
|
||||
class StreamChatState extends State<StreamChat> with WidgetsBindingObserver {
|
||||
Client get client => widget.client;
|
||||
Timer _disconnectTimer;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = _getTheme(context, widget.streamChatThemeData);
|
||||
return StreamChatTheme(
|
||||
data: theme,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final materialTheme = Theme.of(context);
|
||||
final streamTheme = StreamChatTheme.of(context);
|
||||
return Theme(
|
||||
data: materialTheme.copyWith(
|
||||
primaryIconTheme: streamTheme.primaryIconTheme,
|
||||
accentColor: streamTheme.colorTheme.accentBlue,
|
||||
scaffoldBackgroundColor: streamTheme.colorTheme.white,
|
||||
buttonTheme: streamTheme.buttonTheme,
|
||||
),
|
||||
child: widget.child,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
StreamChatThemeData _getTheme(
|
||||
BuildContext context,
|
||||
StreamChatThemeData themeData,
|
||||
) {
|
||||
final defaultTheme = StreamChatThemeData.getDefaultTheme(Theme.of(context));
|
||||
return defaultTheme.merge(themeData) ?? themeData;
|
||||
}
|
||||
|
||||
/// The current user
|
||||
User get user => widget.client.state.user;
|
||||
|
||||
/// The current user as a stream
|
||||
Stream<User> get userStream => widget.client.state.userStream;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
client.state?.totalUnreadCountStream?.listen((count) {
|
||||
if (count > 0) {
|
||||
FlutterAppBadger.updateBadgeCount(count);
|
||||
} else {
|
||||
FlutterAppBadger.removeBadge();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
StreamSubscription _newMessageSubscription;
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (client.state?.user != null) {
|
||||
if (state == AppLifecycleState.paused) {
|
||||
if (client.showLocalNotification != null) {
|
||||
_newMessageSubscription = client
|
||||
.on(EventType.messageNew)
|
||||
.where((e) => e.user?.id != user.id)
|
||||
.where((e) => e.message.silent != true)
|
||||
.where((e) => e.message.shadowed != true)
|
||||
.listen((event) async {
|
||||
final channel = client.channel(
|
||||
event.channelType,
|
||||
id: event.channelId,
|
||||
);
|
||||
|
||||
client.showLocalNotification(
|
||||
event.message,
|
||||
ChannelModel(
|
||||
id: channel.id,
|
||||
createdAt: channel.createdAt,
|
||||
extraData: channel.extraData,
|
||||
type: channel.type,
|
||||
memberCount: channel.memberCount,
|
||||
frozen: channel.frozen,
|
||||
cid: channel.cid,
|
||||
deletedAt: channel.deletedAt,
|
||||
config: channel.config,
|
||||
createdBy: channel.createdBy,
|
||||
updatedAt: channel.updatedAt,
|
||||
lastMessageAt: channel.lastMessageAt,
|
||||
),
|
||||
);
|
||||
});
|
||||
_disconnectTimer = Timer(client.backgroundKeepAlive, () {
|
||||
client.disconnect();
|
||||
});
|
||||
} else {
|
||||
client.disconnect();
|
||||
}
|
||||
} else if (state == AppLifecycleState.resumed) {
|
||||
_newMessageSubscription?.cancel();
|
||||
if (_disconnectTimer?.isActive == true) {
|
||||
_disconnectTimer.cancel();
|
||||
} else {
|
||||
if (client.wsConnectionStatus.value ==
|
||||
ConnectionStatus.disconnected) {
|
||||
NotificationService.handleIosMessageQueue(client);
|
||||
client.connect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,908 +0,0 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/channel_header.dart';
|
||||
import 'package:stream_chat_flutter/src/channel_preview.dart';
|
||||
import 'package:stream_chat_flutter/src/message_input.dart';
|
||||
import 'package:stream_chat_flutter/src/reaction_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/utils.dart';
|
||||
|
||||
/// Inherited widget providing the [StreamChatThemeData] to the widget tree
|
||||
class StreamChatTheme extends InheritedWidget {
|
||||
final StreamChatThemeData data;
|
||||
|
||||
StreamChatTheme({
|
||||
Key key,
|
||||
@required this.data,
|
||||
Widget child,
|
||||
}) : super(
|
||||
key: key,
|
||||
child: child,
|
||||
);
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(StreamChatTheme old) {
|
||||
return data != old.data;
|
||||
}
|
||||
|
||||
/// Use this method to get the current [StreamChatThemeData] instance
|
||||
static StreamChatThemeData of(BuildContext context) {
|
||||
final streamChatTheme =
|
||||
context.dependOnInheritedWidgetOfExactType<StreamChatTheme>();
|
||||
|
||||
if (streamChatTheme == null) {
|
||||
throw Exception(
|
||||
'You must have a StreamChatTheme widget at the top of your widget tree',
|
||||
);
|
||||
}
|
||||
|
||||
return streamChatTheme.data;
|
||||
}
|
||||
}
|
||||
|
||||
/// Theme data
|
||||
class StreamChatThemeData {
|
||||
/// The text themes used in the widgets
|
||||
final TextTheme textTheme;
|
||||
|
||||
/// The button themes used in the widgets
|
||||
final ButtonThemeData buttonTheme;
|
||||
|
||||
/// The text themes used in the widgets
|
||||
final ColorTheme colorTheme;
|
||||
|
||||
/// Theme of the [ChannelPreview]
|
||||
final ChannelPreviewTheme channelPreviewTheme;
|
||||
|
||||
/// Theme of the chat widgets dedicated to a channel
|
||||
final ChannelTheme channelTheme;
|
||||
|
||||
/// Theme of the current user messages
|
||||
final MessageTheme ownMessageTheme;
|
||||
|
||||
/// Theme of other users messages
|
||||
final MessageTheme otherMessageTheme;
|
||||
|
||||
/// The widget that will be built when the channel image is unavailable
|
||||
final Widget Function(BuildContext, Channel) defaultChannelImage;
|
||||
|
||||
/// The widget that will be built when the user image is unavailable
|
||||
final Widget Function(BuildContext, User) defaultUserImage;
|
||||
|
||||
/// Primary icon theme
|
||||
final IconThemeData primaryIconTheme;
|
||||
|
||||
/// Assets used for rendering reactions
|
||||
final List<ReactionIcon> reactionIcons;
|
||||
|
||||
/// Create a theme from scratch
|
||||
const StreamChatThemeData({
|
||||
this.textTheme,
|
||||
this.buttonTheme,
|
||||
this.colorTheme,
|
||||
this.channelPreviewTheme,
|
||||
this.channelTheme,
|
||||
this.otherMessageTheme,
|
||||
this.ownMessageTheme,
|
||||
this.defaultChannelImage,
|
||||
this.defaultUserImage,
|
||||
this.primaryIconTheme,
|
||||
this.reactionIcons,
|
||||
});
|
||||
|
||||
/// Create a theme from a Material [Theme]
|
||||
factory StreamChatThemeData.fromTheme(ThemeData theme) {
|
||||
final defaultTheme = getDefaultTheme(theme);
|
||||
final customizedTheme = StreamChatThemeData(
|
||||
primaryIconTheme: theme.primaryIconTheme,
|
||||
ownMessageTheme: MessageTheme(
|
||||
replies: TextStyle(color: theme.accentColor),
|
||||
messageLinks: TextStyle(color: theme.accentColor),
|
||||
),
|
||||
otherMessageTheme: MessageTheme(
|
||||
replies: TextStyle(color: theme.accentColor),
|
||||
messageLinks: TextStyle(color: theme.accentColor),
|
||||
),
|
||||
);
|
||||
return defaultTheme.merge(customizedTheme) ?? customizedTheme;
|
||||
}
|
||||
|
||||
/// Creates a copy of [StreamChatThemeData] with specified attributes overridden.
|
||||
StreamChatThemeData copyWith({
|
||||
TextTheme textTheme,
|
||||
ButtonThemeData buttonTheme,
|
||||
ColorTheme colorTheme,
|
||||
ChannelPreviewTheme channelPreviewTheme,
|
||||
ChannelTheme channelTheme,
|
||||
MessageTheme ownMessageTheme,
|
||||
MessageTheme otherMessageTheme,
|
||||
Widget Function(BuildContext, Channel) defaultChannelImage,
|
||||
Widget Function(BuildContext, User) defaultUserImage,
|
||||
IconThemeData primaryIconTheme,
|
||||
List<ReactionIcon> reactionIcons,
|
||||
}) =>
|
||||
StreamChatThemeData(
|
||||
textTheme: textTheme ?? this.textTheme,
|
||||
buttonTheme: buttonTheme ?? this.buttonTheme,
|
||||
colorTheme: colorTheme ?? this.colorTheme,
|
||||
primaryIconTheme: primaryIconTheme ?? this.primaryIconTheme,
|
||||
defaultChannelImage: defaultChannelImage ?? this.defaultChannelImage,
|
||||
defaultUserImage: defaultUserImage ?? this.defaultUserImage,
|
||||
channelPreviewTheme: channelPreviewTheme ?? this.channelPreviewTheme,
|
||||
channelTheme: channelTheme ?? this.channelTheme,
|
||||
ownMessageTheme: ownMessageTheme ?? this.ownMessageTheme,
|
||||
otherMessageTheme: otherMessageTheme ?? this.otherMessageTheme,
|
||||
reactionIcons: reactionIcons ?? this.reactionIcons,
|
||||
);
|
||||
|
||||
StreamChatThemeData merge(StreamChatThemeData other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
textTheme: textTheme?.merge(other.textTheme) ?? other.textTheme,
|
||||
buttonTheme: other.buttonTheme,
|
||||
colorTheme: colorTheme?.merge(other.colorTheme) ?? other.colorTheme,
|
||||
primaryIconTheme: other.primaryIconTheme,
|
||||
defaultChannelImage: other.defaultChannelImage,
|
||||
defaultUserImage: other.defaultUserImage,
|
||||
channelPreviewTheme:
|
||||
channelPreviewTheme?.merge(other.channelPreviewTheme) ??
|
||||
other.channelPreviewTheme,
|
||||
channelTheme:
|
||||
channelTheme?.merge(other.channelTheme) ?? other.channelTheme,
|
||||
ownMessageTheme: ownMessageTheme?.merge(other.ownMessageTheme) ??
|
||||
other.ownMessageTheme,
|
||||
otherMessageTheme: otherMessageTheme?.merge(other.otherMessageTheme) ??
|
||||
other.otherMessageTheme,
|
||||
reactionIcons: other.reactionIcons,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get the default Stream Chat theme
|
||||
static StreamChatThemeData getDefaultTheme(ThemeData theme) {
|
||||
final accentColor = Color(0xff006cff);
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
final textTheme = isDark ? TextTheme.dark() : TextTheme.light();
|
||||
final colorTheme = isDark ? ColorTheme.dark() : ColorTheme.light();
|
||||
return StreamChatThemeData(
|
||||
textTheme: textTheme,
|
||||
colorTheme: colorTheme,
|
||||
buttonTheme: ButtonThemeData(
|
||||
height: 48.0,
|
||||
buttonColor: isDark ? Color(0xffffffff) : Color(0xff006aff),
|
||||
textTheme: ButtonTextTheme.accent,
|
||||
colorScheme: theme.colorScheme.copyWith(
|
||||
secondary: isDark ? Color(0xff005eff) : Color(0xffffffff),
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
),
|
||||
),
|
||||
primaryIconTheme: IconThemeData(color: colorTheme.black.withOpacity(.5)),
|
||||
defaultChannelImage: (context, channel) => SizedBox(),
|
||||
defaultUserImage: (context, user) => Center(
|
||||
child: CachedNetworkImage(
|
||||
filterQuality: FilterQuality.high,
|
||||
imageUrl: getRandomPicUrl(user),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
channelPreviewTheme: ChannelPreviewTheme(
|
||||
unreadCounterColor: colorTheme.accentRed,
|
||||
avatarTheme: AvatarTheme(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 40,
|
||||
width: 40,
|
||||
),
|
||||
),
|
||||
title: textTheme.bodyBold,
|
||||
subtitle: textTheme.footnote.copyWith(
|
||||
color: Color(0xff7A7A7A),
|
||||
),
|
||||
lastMessageAt: textTheme.footnote.copyWith(
|
||||
color: colorTheme.black.withOpacity(.5),
|
||||
),
|
||||
indicatorIconSize: 16.0),
|
||||
channelTheme: ChannelTheme(
|
||||
messageInputButtonIconTheme: theme.iconTheme.copyWith(
|
||||
color: accentColor,
|
||||
),
|
||||
channelHeaderTheme: ChannelHeaderTheme(
|
||||
avatarTheme: AvatarTheme(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 40,
|
||||
width: 40,
|
||||
),
|
||||
),
|
||||
color: colorTheme.white,
|
||||
title: TextStyle(
|
||||
fontSize: 14,
|
||||
color: colorTheme.black,
|
||||
),
|
||||
lastMessageAt: TextStyle(
|
||||
fontSize: 11,
|
||||
color: colorTheme.black.withOpacity(.5),
|
||||
),
|
||||
),
|
||||
inputBackground: colorTheme.white.withAlpha(12),
|
||||
),
|
||||
ownMessageTheme: MessageTheme(
|
||||
messageText: TextStyle(
|
||||
fontSize: 14.5,
|
||||
color: colorTheme.black,
|
||||
),
|
||||
createdAt: TextStyle(
|
||||
color: colorTheme.black.withOpacity(.5),
|
||||
fontSize: 12,
|
||||
),
|
||||
replies: TextStyle(
|
||||
color: accentColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
messageBackgroundColor: colorTheme.greyGainsboro,
|
||||
reactionsBackgroundColor: colorTheme.white,
|
||||
reactionsBorderColor: colorTheme.greyWhisper,
|
||||
messageBorderColor: colorTheme.greyGainsboro,
|
||||
avatarTheme: AvatarTheme(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 32,
|
||||
width: 32,
|
||||
),
|
||||
),
|
||||
messageLinks: TextStyle(
|
||||
color: accentColor,
|
||||
),
|
||||
),
|
||||
otherMessageTheme: MessageTheme(
|
||||
reactionsBackgroundColor: colorTheme.greyGainsboro,
|
||||
reactionsBorderColor: colorTheme.white,
|
||||
messageText: TextStyle(
|
||||
fontSize: 14.5,
|
||||
color: colorTheme.black,
|
||||
),
|
||||
createdAt: TextStyle(
|
||||
color: colorTheme.black.withOpacity(.5),
|
||||
fontSize: 12,
|
||||
),
|
||||
replies: TextStyle(
|
||||
color: accentColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12,
|
||||
),
|
||||
messageLinks: TextStyle(
|
||||
color: accentColor,
|
||||
),
|
||||
messageBackgroundColor: colorTheme.white,
|
||||
messageBorderColor: colorTheme.greyWhisper,
|
||||
avatarTheme: AvatarTheme(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 32,
|
||||
width: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
reactionIcons: [
|
||||
ReactionIcon(
|
||||
type: 'love',
|
||||
assetName: 'Icon_love_reaction.svg',
|
||||
),
|
||||
ReactionIcon(
|
||||
type: 'like',
|
||||
assetName: 'Icon_thumbs_up_reaction.svg',
|
||||
),
|
||||
ReactionIcon(
|
||||
type: 'sad',
|
||||
assetName: 'Icon_thumbs_down_reaction.svg',
|
||||
),
|
||||
ReactionIcon(
|
||||
type: 'haha',
|
||||
assetName: 'Icon_LOL_reaction.svg',
|
||||
),
|
||||
ReactionIcon(
|
||||
type: 'wow',
|
||||
assetName: 'Icon_wut_reaction.svg',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum TextThemeType {
|
||||
light,
|
||||
dark,
|
||||
}
|
||||
|
||||
class TextTheme {
|
||||
final TextStyle title;
|
||||
final TextStyle headlineBold;
|
||||
final TextStyle headline;
|
||||
final TextStyle bodyBold;
|
||||
final TextStyle body;
|
||||
final TextStyle footnoteBold;
|
||||
final TextStyle footnote;
|
||||
final TextStyle captionBold;
|
||||
|
||||
TextTheme.light({
|
||||
this.title = const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
),
|
||||
this.headlineBold = const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
),
|
||||
this.headline = const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black,
|
||||
),
|
||||
this.bodyBold = const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
),
|
||||
this.body = const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black,
|
||||
),
|
||||
this.footnoteBold = const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black,
|
||||
),
|
||||
this.footnote = const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black,
|
||||
),
|
||||
this.captionBold = const TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black,
|
||||
),
|
||||
});
|
||||
|
||||
TextTheme.dark({
|
||||
this.title = const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
this.headlineBold = const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
this.headline = const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
this.bodyBold = const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
this.body = const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
this.footnoteBold = const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
this.footnote = const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.white,
|
||||
),
|
||||
this.captionBold = const TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
});
|
||||
|
||||
TextTheme copyWith({
|
||||
TextThemeType type = TextThemeType.light,
|
||||
TextStyle body,
|
||||
TextStyle title,
|
||||
TextStyle headlineBold,
|
||||
TextStyle headline,
|
||||
TextStyle bodyBold,
|
||||
TextStyle footnoteBold,
|
||||
TextStyle footnote,
|
||||
TextStyle captionBold,
|
||||
}) {
|
||||
return type == TextThemeType.light
|
||||
? TextTheme.light(
|
||||
body: body ?? this.body,
|
||||
title: title ?? this.title,
|
||||
headlineBold: headlineBold ?? this.headlineBold,
|
||||
headline: headline ?? this.headline,
|
||||
bodyBold: bodyBold ?? this.bodyBold,
|
||||
footnoteBold: footnoteBold ?? this.footnoteBold,
|
||||
footnote: footnote ?? this.footnote,
|
||||
captionBold: captionBold ?? this.captionBold,
|
||||
)
|
||||
: TextTheme.dark(
|
||||
body: body ?? this.body,
|
||||
title: title ?? this.title,
|
||||
headlineBold: headlineBold ?? this.headlineBold,
|
||||
headline: headline ?? this.headline,
|
||||
bodyBold: bodyBold ?? this.bodyBold,
|
||||
footnoteBold: footnoteBold ?? this.footnoteBold,
|
||||
footnote: footnote ?? this.footnote,
|
||||
captionBold: captionBold ?? this.captionBold,
|
||||
);
|
||||
}
|
||||
|
||||
TextTheme merge(TextTheme other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
body: body?.merge(other.body) ?? other.body,
|
||||
title: title?.merge(other.title) ?? other.title,
|
||||
headlineBold:
|
||||
headlineBold?.merge(other.headlineBold) ?? other.headlineBold,
|
||||
headline: headline?.merge(other.headline) ?? other.headline,
|
||||
bodyBold: bodyBold?.merge(other.bodyBold) ?? other.bodyBold,
|
||||
footnoteBold:
|
||||
footnoteBold?.merge(other.footnoteBold) ?? other.footnoteBold,
|
||||
footnote: footnote?.merge(other.footnote) ?? other.footnote,
|
||||
captionBold: captionBold?.merge(other.captionBold) ?? other.captionBold,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum ColorThemeType {
|
||||
light,
|
||||
dark,
|
||||
}
|
||||
|
||||
class ColorTheme {
|
||||
final Color black;
|
||||
final Color grey;
|
||||
final Color greyGainsboro;
|
||||
final Color greyWhisper;
|
||||
final Color whiteSmoke;
|
||||
final Color whiteSnow;
|
||||
final Color white;
|
||||
final Color blueAlice;
|
||||
final Color accentBlue;
|
||||
final Color accentRed;
|
||||
final Color accentGreen;
|
||||
final Effect borderTop;
|
||||
final Effect borderBottom;
|
||||
final Effect shadowIconButton;
|
||||
final Effect modalShadow;
|
||||
final Color highlight;
|
||||
final Color overlay;
|
||||
final Color overlayDark;
|
||||
final Gradient bgGradient;
|
||||
|
||||
ColorTheme.light({
|
||||
this.black = const Color(0xff000000),
|
||||
this.grey = const Color(0xff7a7a7a),
|
||||
this.greyGainsboro = const Color(0xffdbdbdb),
|
||||
this.greyWhisper = const Color(0xffecebeb),
|
||||
this.whiteSmoke = const Color(0xfff2f2f2),
|
||||
this.whiteSnow = const Color(0xfffcfcfc),
|
||||
this.white = const Color(0xffffffff),
|
||||
this.blueAlice = const Color(0xffe9f2ff),
|
||||
this.accentBlue = const Color(0xff005FFF),
|
||||
this.accentRed = const Color(0xffFF3842),
|
||||
this.accentGreen = const Color(0xff20E070),
|
||||
this.highlight = const Color(0xfffbf4dd),
|
||||
this.overlay = const Color.fromRGBO(0, 0, 0, 0.2),
|
||||
this.overlayDark = const Color.fromRGBO(0, 0, 0, 0.6),
|
||||
this.bgGradient = const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [const Color(0xfff7f7f7), const Color(0xfffcfcfc)],
|
||||
stops: [0, 1],
|
||||
),
|
||||
this.borderTop = const Effect(
|
||||
sigmaX: 0, sigmaY: -1, color: Color(0xff141924), blur: 0.0),
|
||||
this.borderBottom =
|
||||
const Effect(sigmaX: 0, sigmaY: 1, color: Color(0xff141924), blur: 0.0),
|
||||
this.shadowIconButton = const Effect(
|
||||
sigmaX: 0, sigmaY: 2, color: Color(0xff000000), alpha: 0.5, blur: 4.0),
|
||||
this.modalShadow = const Effect(
|
||||
sigmaX: 0, sigmaY: 0, color: Color(0xff000000), alpha: 1, blur: 8.0),
|
||||
});
|
||||
|
||||
ColorTheme.dark({
|
||||
this.black = const Color(0xffffffff),
|
||||
this.grey = const Color(0xff7a7a7a),
|
||||
this.greyGainsboro = const Color(0xff2d2f2f),
|
||||
this.greyWhisper = const Color(0xff1c1e22),
|
||||
this.whiteSmoke = const Color(0xff13151b),
|
||||
this.whiteSnow = const Color(0xff070A0D),
|
||||
this.white = const Color(0xff101418),
|
||||
this.blueAlice = const Color(0xff00193D),
|
||||
this.accentBlue = const Color(0xff005FFF),
|
||||
this.accentRed = const Color(0xffFF3742),
|
||||
this.accentGreen = const Color(0xff20E070),
|
||||
this.borderTop = const Effect(
|
||||
sigmaX: 0, sigmaY: -1, color: Color(0xff141924), blur: 0.0),
|
||||
this.borderBottom =
|
||||
const Effect(sigmaX: 0, sigmaY: 1, color: Color(0xff141924), blur: 0.0),
|
||||
this.shadowIconButton = const Effect(
|
||||
sigmaX: 0, sigmaY: 2, color: Color(0xff000000), alpha: 0.5, blur: 4.0),
|
||||
this.modalShadow = const Effect(
|
||||
sigmaX: 0, sigmaY: 0, color: Color(0xff000000), alpha: 1, blur: 8.0),
|
||||
this.highlight = const Color(0xff302d22),
|
||||
this.overlay = const Color.fromRGBO(0, 0, 0, 0.4),
|
||||
this.overlayDark = const Color.fromRGBO(255, 255, 255, 0.6),
|
||||
this.bgGradient = const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [const Color(0xff101214), const Color(0xff070a0d)],
|
||||
stops: [0, 1],
|
||||
),
|
||||
});
|
||||
|
||||
ColorTheme copyWith({
|
||||
ColorThemeType type = ColorThemeType.light,
|
||||
Color black,
|
||||
Color grey,
|
||||
Color greyGainsboro,
|
||||
Color greyWhisper,
|
||||
Color whiteSmoke,
|
||||
Color whiteSnow,
|
||||
Color white,
|
||||
Color blueAlice,
|
||||
Color accentBlue,
|
||||
Color accentRed,
|
||||
Color accentGreen,
|
||||
Effect borderTop,
|
||||
Effect borderBottom,
|
||||
Effect shadowIconButton,
|
||||
Effect modalShadow,
|
||||
Color highlight,
|
||||
Color overlay,
|
||||
Color overlayDark,
|
||||
Gradient bgGradient,
|
||||
}) {
|
||||
return type == ColorThemeType.light
|
||||
? ColorTheme.light(
|
||||
black: black ?? this.black,
|
||||
grey: grey ?? this.grey,
|
||||
greyGainsboro: greyGainsboro ?? this.greyGainsboro,
|
||||
greyWhisper: greyWhisper ?? this.greyWhisper,
|
||||
whiteSmoke: whiteSmoke ?? this.whiteSmoke,
|
||||
whiteSnow: whiteSnow ?? this.whiteSnow,
|
||||
white: white ?? this.white,
|
||||
blueAlice: blueAlice ?? this.blueAlice,
|
||||
accentBlue: accentBlue ?? this.accentBlue,
|
||||
accentRed: accentRed ?? this.accentRed,
|
||||
accentGreen: accentGreen ?? this.accentGreen,
|
||||
borderTop: borderTop ?? this.borderTop,
|
||||
borderBottom: borderBottom ?? this.borderBottom,
|
||||
shadowIconButton: shadowIconButton ?? this.shadowIconButton,
|
||||
modalShadow: modalShadow ?? this.modalShadow,
|
||||
highlight: highlight ?? this.highlight,
|
||||
overlay: overlay ?? this.overlay,
|
||||
overlayDark: overlayDark ?? this.overlayDark,
|
||||
bgGradient: bgGradient ?? this.bgGradient,
|
||||
)
|
||||
: ColorTheme.dark(
|
||||
black: black ?? this.black,
|
||||
grey: grey ?? this.grey,
|
||||
greyGainsboro: greyGainsboro ?? this.greyGainsboro,
|
||||
greyWhisper: greyWhisper ?? this.greyWhisper,
|
||||
whiteSmoke: whiteSmoke ?? this.whiteSmoke,
|
||||
whiteSnow: whiteSnow ?? this.whiteSnow,
|
||||
white: white ?? this.white,
|
||||
blueAlice: blueAlice ?? this.blueAlice,
|
||||
accentBlue: accentBlue ?? this.accentBlue,
|
||||
accentRed: accentRed ?? this.accentRed,
|
||||
accentGreen: accentGreen ?? this.accentGreen,
|
||||
borderTop: borderTop ?? this.borderTop,
|
||||
borderBottom: borderBottom ?? this.borderBottom,
|
||||
shadowIconButton: shadowIconButton ?? this.shadowIconButton,
|
||||
modalShadow: modalShadow ?? this.modalShadow,
|
||||
highlight: highlight ?? this.highlight,
|
||||
overlay: overlay ?? this.overlay,
|
||||
overlayDark: overlayDark ?? this.overlayDark,
|
||||
bgGradient: bgGradient ?? this.bgGradient,
|
||||
);
|
||||
}
|
||||
|
||||
ColorTheme merge(ColorTheme other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
black: other.black,
|
||||
grey: other.grey,
|
||||
greyGainsboro: other.greyGainsboro,
|
||||
greyWhisper: other.greyWhisper,
|
||||
whiteSmoke: other.whiteSmoke,
|
||||
whiteSnow: other.whiteSnow,
|
||||
white: other.white,
|
||||
blueAlice: other.blueAlice,
|
||||
accentBlue: other.accentBlue,
|
||||
accentRed: other.accentRed,
|
||||
accentGreen: other.accentGreen,
|
||||
highlight: other.highlight,
|
||||
overlay: other.overlay,
|
||||
overlayDark: other.overlayDark,
|
||||
bgGradient: other.bgGradient,
|
||||
borderTop: other.borderTop,
|
||||
borderBottom: other.borderBottom,
|
||||
shadowIconButton: other.shadowIconButton,
|
||||
modalShadow: other.modalShadow,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Channel theme data
|
||||
class ChannelTheme {
|
||||
/// Theme of the [ChannelHeader] widget
|
||||
final ChannelHeaderTheme channelHeaderTheme;
|
||||
|
||||
/// IconTheme of the send button in [MessageInput]
|
||||
final IconThemeData messageInputButtonIconTheme;
|
||||
|
||||
/// Theme of the send button in [MessageInput]
|
||||
final ButtonThemeData messageInputButtonTheme;
|
||||
|
||||
/// Background color of [MessageInput]
|
||||
final Color inputBackground;
|
||||
|
||||
ChannelTheme({
|
||||
this.channelHeaderTheme,
|
||||
this.messageInputButtonIconTheme,
|
||||
this.messageInputButtonTheme,
|
||||
this.inputBackground,
|
||||
});
|
||||
|
||||
/// Creates a copy of [ChannelTheme] with specified attributes overridden.
|
||||
ChannelTheme copyWith({
|
||||
ChannelHeaderTheme channelHeaderTheme,
|
||||
IconThemeData messageInputButtonIconTheme,
|
||||
ButtonThemeData messageInputButtonTheme,
|
||||
Color inputBackground,
|
||||
}) =>
|
||||
ChannelTheme(
|
||||
channelHeaderTheme: channelHeaderTheme ?? this.channelHeaderTheme,
|
||||
messageInputButtonIconTheme:
|
||||
messageInputButtonIconTheme ?? this.messageInputButtonIconTheme,
|
||||
messageInputButtonTheme:
|
||||
messageInputButtonTheme ?? this.messageInputButtonTheme,
|
||||
inputBackground: inputBackground ?? this.inputBackground,
|
||||
);
|
||||
|
||||
ChannelTheme merge(ChannelTheme other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
channelHeaderTheme: channelHeaderTheme?.merge(other.channelHeaderTheme) ??
|
||||
other.channelHeaderTheme,
|
||||
messageInputButtonIconTheme: messageInputButtonIconTheme
|
||||
?.merge(other.messageInputButtonIconTheme) ??
|
||||
other.messageInputButtonIconTheme,
|
||||
messageInputButtonTheme: other.messageInputButtonTheme,
|
||||
inputBackground: other.inputBackground,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AvatarTheme {
|
||||
final BoxConstraints constraints;
|
||||
final BorderRadius borderRadius;
|
||||
|
||||
AvatarTheme({
|
||||
this.constraints,
|
||||
this.borderRadius,
|
||||
});
|
||||
|
||||
AvatarTheme copyWith({
|
||||
BoxConstraints constraints,
|
||||
BorderRadius borderRadius,
|
||||
}) =>
|
||||
AvatarTheme(
|
||||
constraints: constraints ?? this.constraints,
|
||||
borderRadius: borderRadius ?? this.borderRadius,
|
||||
);
|
||||
|
||||
AvatarTheme merge(AvatarTheme other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
constraints: other.constraints,
|
||||
borderRadius: other.borderRadius,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MessageTheme {
|
||||
final TextStyle messageText;
|
||||
final TextStyle messageAuthor;
|
||||
final TextStyle messageLinks;
|
||||
final TextStyle createdAt;
|
||||
final TextStyle replies;
|
||||
final Color messageBackgroundColor;
|
||||
final Color messageBorderColor;
|
||||
final Color reactionsBackgroundColor;
|
||||
final Color reactionsBorderColor;
|
||||
final AvatarTheme avatarTheme;
|
||||
|
||||
const MessageTheme({
|
||||
this.replies,
|
||||
this.messageText,
|
||||
this.messageAuthor,
|
||||
this.messageLinks,
|
||||
this.messageBackgroundColor,
|
||||
this.messageBorderColor,
|
||||
this.reactionsBackgroundColor,
|
||||
this.reactionsBorderColor,
|
||||
this.avatarTheme,
|
||||
this.createdAt,
|
||||
});
|
||||
|
||||
MessageTheme copyWith({
|
||||
TextStyle messageText,
|
||||
TextStyle messageAuthor,
|
||||
TextStyle messageLinks,
|
||||
TextStyle createdAt,
|
||||
TextStyle replies,
|
||||
Color messageBackgroundColor,
|
||||
Color messageBorderColor,
|
||||
AvatarTheme avatarTheme,
|
||||
Color reactionsBackgroundColor,
|
||||
Color reactionsBorderColor,
|
||||
}) =>
|
||||
MessageTheme(
|
||||
messageText: messageText ?? this.messageText,
|
||||
messageAuthor: messageAuthor ?? this.messageAuthor,
|
||||
messageLinks: messageLinks ?? this.messageLinks,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
messageBackgroundColor:
|
||||
messageBackgroundColor ?? this.messageBackgroundColor,
|
||||
messageBorderColor: messageBorderColor ?? this.messageBorderColor,
|
||||
avatarTheme: avatarTheme ?? this.avatarTheme,
|
||||
replies: replies ?? this.replies,
|
||||
reactionsBackgroundColor:
|
||||
reactionsBackgroundColor ?? this.reactionsBackgroundColor,
|
||||
reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor,
|
||||
);
|
||||
|
||||
MessageTheme merge(MessageTheme other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
messageText: messageText?.merge(other.messageText) ?? other.messageText,
|
||||
messageAuthor:
|
||||
messageAuthor?.merge(other.messageAuthor) ?? other.messageAuthor,
|
||||
messageLinks:
|
||||
messageLinks?.merge(other.messageLinks) ?? other.messageLinks,
|
||||
createdAt: createdAt?.merge(other.createdAt) ?? other.createdAt,
|
||||
replies: replies?.merge(other.replies) ?? other.replies,
|
||||
messageBackgroundColor: other.messageBackgroundColor,
|
||||
messageBorderColor: other.messageBorderColor,
|
||||
avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme,
|
||||
reactionsBackgroundColor: other.reactionsBackgroundColor,
|
||||
reactionsBorderColor: other.reactionsBorderColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelPreviewTheme {
|
||||
final TextStyle title;
|
||||
final TextStyle subtitle;
|
||||
final TextStyle lastMessageAt;
|
||||
final AvatarTheme avatarTheme;
|
||||
final Color unreadCounterColor;
|
||||
final double indicatorIconSize;
|
||||
|
||||
const ChannelPreviewTheme({
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.lastMessageAt,
|
||||
this.avatarTheme,
|
||||
this.unreadCounterColor,
|
||||
this.indicatorIconSize,
|
||||
});
|
||||
|
||||
ChannelPreviewTheme copyWith({
|
||||
TextStyle title,
|
||||
TextStyle subtitle,
|
||||
TextStyle lastMessageAt,
|
||||
AvatarTheme avatarTheme,
|
||||
Color unreadCounterColor,
|
||||
double indicatorIconSize,
|
||||
}) =>
|
||||
ChannelPreviewTheme(
|
||||
title: title ?? this.title,
|
||||
subtitle: subtitle ?? this.subtitle,
|
||||
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
|
||||
avatarTheme: avatarTheme ?? this.avatarTheme,
|
||||
unreadCounterColor: unreadCounterColor ?? this.unreadCounterColor,
|
||||
indicatorIconSize: indicatorIconSize ?? this.indicatorIconSize,
|
||||
);
|
||||
|
||||
ChannelPreviewTheme merge(ChannelPreviewTheme other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
title: title?.merge(other.title) ?? other.title,
|
||||
subtitle: subtitle?.merge(other.subtitle) ?? other.subtitle,
|
||||
lastMessageAt:
|
||||
lastMessageAt?.merge(other.lastMessageAt) ?? other.lastMessageAt,
|
||||
avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme,
|
||||
unreadCounterColor: other.unreadCounterColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelHeaderTheme {
|
||||
final TextStyle title;
|
||||
final TextStyle lastMessageAt;
|
||||
final AvatarTheme avatarTheme;
|
||||
final Color color;
|
||||
|
||||
const ChannelHeaderTheme({
|
||||
this.title,
|
||||
this.lastMessageAt,
|
||||
this.avatarTheme,
|
||||
this.color,
|
||||
});
|
||||
|
||||
ChannelHeaderTheme copyWith({
|
||||
TextStyle title,
|
||||
TextStyle lastMessageAt,
|
||||
AvatarTheme avatarTheme,
|
||||
Color color,
|
||||
}) =>
|
||||
ChannelHeaderTheme(
|
||||
title: title ?? this.title,
|
||||
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
|
||||
avatarTheme: avatarTheme ?? this.avatarTheme,
|
||||
color: color ?? this.color,
|
||||
);
|
||||
|
||||
ChannelHeaderTheme merge(ChannelHeaderTheme other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
title: title?.merge(other.title) ?? other.title,
|
||||
lastMessageAt:
|
||||
lastMessageAt?.merge(other.lastMessageAt) ?? other.lastMessageAt,
|
||||
avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme,
|
||||
color: other.color,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Effect {
|
||||
final double sigmaX;
|
||||
final double sigmaY;
|
||||
final Color color;
|
||||
final double alpha;
|
||||
final double blur;
|
||||
|
||||
const Effect({
|
||||
this.sigmaX,
|
||||
this.sigmaY,
|
||||
this.color,
|
||||
this.alpha,
|
||||
this.blur,
|
||||
});
|
||||
|
||||
Effect copyWith({
|
||||
double sigmaX,
|
||||
double sigmaY,
|
||||
Color color,
|
||||
double alpha,
|
||||
double blur,
|
||||
}) =>
|
||||
Effect(
|
||||
sigmaX: sigmaX ?? this.sigmaX,
|
||||
sigmaY: sigmaY ?? this.sigmaY,
|
||||
color: color ?? this.color,
|
||||
alpha: color ?? this.alpha,
|
||||
blur: blur ?? this.blur,
|
||||
);
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class StreamNeumorphicButton extends StatelessWidget {
|
||||
final Widget child;
|
||||
final Color backgroundColor;
|
||||
|
||||
const StreamNeumorphicButton({
|
||||
Key key,
|
||||
@required this.child,
|
||||
this.backgroundColor = Colors.white,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
child: child,
|
||||
margin: EdgeInsets.all(8.0),
|
||||
height: 40,
|
||||
width: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey[700],
|
||||
offset: Offset(0, 1.0),
|
||||
blurRadius: 0.5,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
BoxShadow(
|
||||
color: Colors.white,
|
||||
offset: Offset.zero,
|
||||
blurRadius: 0.5,
|
||||
spreadRadius: 0,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,832 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class StreamSvgIcon extends StatelessWidget {
|
||||
final String assetName;
|
||||
final double width;
|
||||
final double height;
|
||||
final Color color;
|
||||
|
||||
const StreamSvgIcon({
|
||||
this.assetName,
|
||||
this.color,
|
||||
this.width = 24,
|
||||
this.height = 24,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final key = Key('StreamSvgIcon-$assetName');
|
||||
return kIsWeb
|
||||
? Image.network(
|
||||
'packages/stream_chat_flutter/svgs/$assetName',
|
||||
width: width,
|
||||
height: height,
|
||||
key: key,
|
||||
color: color,
|
||||
alignment: Alignment.center,
|
||||
)
|
||||
: SvgPicture.asset(
|
||||
'lib/svgs/$assetName',
|
||||
package: 'stream_chat_flutter',
|
||||
key: key,
|
||||
width: width,
|
||||
height: height,
|
||||
color: color,
|
||||
alignment: Alignment.center,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.settings({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'settings.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.down({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_down.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.attach({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_attach.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.smile({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_smile.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.mentions({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'mentions.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.record({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_record.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.camera({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_camera.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.files({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'files.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.pictures({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'pictures.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.left({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_left.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.user({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_user.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.userAdd({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_User_add.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.check({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_check.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.checkAll({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_check_all.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.checkSend({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_check_send.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.penWrite({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_pen-write.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.contacts({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_contacts.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.close({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_close.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.search({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_search.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.right({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_right.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.mute({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_mute.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.userRemove({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_User_deselect.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.lightning({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_lightning-command runner.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.emptyCircleLeft({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_empty_circle_left.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.message({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_message.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.thread({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_Thread_Reply.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.reply({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_curve_line_left_up_big.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.edit({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_edit.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.download({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_download.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.cloud_download({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_cloud_download.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.copy({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_copy.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.delete({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_delete.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.eye({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_eye-off.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.arrow_right({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_arrow_right.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.close_small({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_close_sml.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.Icon_curve_line_left_up({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_curve_line_left_up.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.icon_SHARE({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'icon_SHARE.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.Icon_grid({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_grid.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.Icon_send_message({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_send_message.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.Icon_menu_point_v({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_menu_point_v.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.Icon_save({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_save.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.share_arrow({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'share_arrow.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.filetype_7z({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'filetype_7z.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.filetype_csv({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'filetype_CSV.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.filetype_doc({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'filetype_DOC.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.filetype_docx({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'filetype_DOCX.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.filetype_Generic({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'filetype_Generic.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.filetype_html({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'filetype_html.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.filetype_md({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'filetype_MD.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.filetype_odt({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'filetype_ODT.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.filetype_pdf({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'filetype_PDF.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.filetype_ppt({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'filetype_PPT.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.filetype_pptx({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'filetype_PPTX.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.filetype_rar({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'filetype_RAR.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.filetype_rtf({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'filetype_RTF.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.filetype_tar({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'filetype_TAR.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.filetype_txt({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'filetype_TXT.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.filetype_xls({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'filetype_XLS.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.filetype_xlsx({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'filetype_XLSX.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.filetype_zip({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'filetype_ZIP.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.Icon_group({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_group.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.Icon_notification({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_notification.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.Icon_user_delete({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_user_delete.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.error({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_error.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.circle_up({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_circle_up.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
|
||||
factory StreamSvgIcon.Icon_user_settings({
|
||||
double size,
|
||||
Color color,
|
||||
}) {
|
||||
return StreamSvgIcon(
|
||||
assetName: 'Icon_user_settings.svg',
|
||||
color: color,
|
||||
width: size,
|
||||
height: size,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'stream_chat_theme.dart';
|
||||
|
||||
///
|
||||
class Swipeable extends StatefulWidget {
|
||||
final Widget child;
|
||||
final Widget backgroundIcon;
|
||||
final VoidCallback onSwipeStart;
|
||||
final VoidCallback onSwipeCancel;
|
||||
final VoidCallback onSwipeEnd;
|
||||
final double threshold;
|
||||
|
||||
///
|
||||
const Swipeable({
|
||||
@required this.child,
|
||||
@required this.backgroundIcon,
|
||||
this.onSwipeStart,
|
||||
this.onSwipeCancel,
|
||||
this.onSwipeEnd,
|
||||
this.threshold = 82.0,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _SwipeableState();
|
||||
}
|
||||
|
||||
class _SwipeableState extends State<Swipeable> with TickerProviderStateMixin {
|
||||
double _dragExtent = 0.0;
|
||||
AnimationController _moveController;
|
||||
AnimationController _iconMoveController;
|
||||
Animation<Offset> _moveAnimation;
|
||||
Animation<Offset> _iconTransitionAnimation;
|
||||
Animation<double> _iconFadeAnimation;
|
||||
bool _pastThreshold = false;
|
||||
|
||||
final _animationDuration = const Duration(milliseconds: 200);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_moveController =
|
||||
AnimationController(duration: _animationDuration, vsync: this);
|
||||
_iconMoveController =
|
||||
AnimationController(duration: _animationDuration, vsync: this);
|
||||
_moveAnimation = Tween<Offset>(begin: Offset.zero, end: Offset(1.0, 0.0))
|
||||
.animate(_moveController);
|
||||
_iconTransitionAnimation =
|
||||
Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset(0.4, 0.0))
|
||||
.animate(_moveController);
|
||||
_iconFadeAnimation =
|
||||
Tween<double>(begin: 0.7, end: 1.0).animate(_iconMoveController);
|
||||
|
||||
final controllerValue = 0.0;
|
||||
_moveController.animateTo(controllerValue);
|
||||
_iconMoveController.animateTo(controllerValue);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_moveController.dispose();
|
||||
_iconMoveController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleDragStart(DragStartDetails details) {
|
||||
if (widget.onSwipeStart != null) {
|
||||
widget.onSwipeStart();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleDragUpdate(DragUpdateDetails details) {
|
||||
final delta = details.primaryDelta;
|
||||
_dragExtent += delta;
|
||||
|
||||
if (_dragExtent.isNegative) return;
|
||||
|
||||
var movePastThresholdPixels = widget.threshold;
|
||||
var newPos = _dragExtent.abs() / context.size.width;
|
||||
|
||||
if (_dragExtent.abs() > movePastThresholdPixels) {
|
||||
// how many "thresholds" past the threshold we are. 1 = the threshold 2
|
||||
// = two thresholds.
|
||||
var n = _dragExtent.abs() / movePastThresholdPixels;
|
||||
|
||||
// Take the number of thresholds past the threshold, and reduce this
|
||||
// number
|
||||
var reducedThreshold = math.pow(n, 0.3);
|
||||
|
||||
var adjustedPixelPos = movePastThresholdPixels * reducedThreshold;
|
||||
newPos = adjustedPixelPos / context.size.width;
|
||||
|
||||
if (_dragExtent > 0 && !_pastThreshold) {
|
||||
_iconMoveController.value = 1;
|
||||
_pastThreshold = true;
|
||||
}
|
||||
} else {
|
||||
// Send a cancel event if the user has swiped back underneath the
|
||||
// threshold
|
||||
if (_pastThreshold && widget.onSwipeCancel != null) {
|
||||
widget.onSwipeCancel();
|
||||
}
|
||||
_pastThreshold = false;
|
||||
}
|
||||
if (!_pastThreshold || newPos < _moveController.value) {
|
||||
_iconMoveController.value = newPos;
|
||||
}
|
||||
_moveController.value = newPos;
|
||||
}
|
||||
|
||||
void _handleDragEnd(DragEndDetails details) {
|
||||
_moveController.animateTo(0.0, duration: _animationDuration);
|
||||
_iconMoveController.animateTo(0.0, duration: _animationDuration);
|
||||
_dragExtent = 0.0;
|
||||
if (_pastThreshold && widget.onSwipeEnd != null) {
|
||||
widget.onSwipeEnd();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onHorizontalDragStart: _handleDragStart,
|
||||
onHorizontalDragUpdate: _handleDragUpdate,
|
||||
onHorizontalDragEnd: _handleDragEnd,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
fit: StackFit.passthrough,
|
||||
children: [
|
||||
SlideTransition(
|
||||
position: _iconTransitionAnimation,
|
||||
child: Row(
|
||||
children: [
|
||||
FadeTransition(
|
||||
opacity: _iconFadeAnimation,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.greyGainsboro,
|
||||
),
|
||||
),
|
||||
child: widget.backgroundIcon,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SlideTransition(
|
||||
position: _moveAnimation,
|
||||
child: widget.child,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// It shows a date divider depending on the date difference
|
||||
class SystemMessage extends StatelessWidget {
|
||||
/// This message
|
||||
final Message message;
|
||||
|
||||
/// The function called when tapping on the message when the message is not failed
|
||||
final void Function(Message) onMessageTap;
|
||||
|
||||
const SystemMessage({
|
||||
Key key,
|
||||
@required this.message,
|
||||
this.onMessageTap,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final divider = Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Divider(),
|
||||
),
|
||||
);
|
||||
|
||||
final createdAt = Jiffy(message.createdAt.toLocal());
|
||||
final now = DateTime.now();
|
||||
final hourInfo = createdAt.format('h:mm a');
|
||||
|
||||
String dayInfo;
|
||||
if (Jiffy(createdAt).isSame(now, Units.DAY)) {
|
||||
dayInfo = 'TODAY';
|
||||
} else if (Jiffy(createdAt)
|
||||
.isSame(now.subtract(Duration(days: 1)), Units.DAY)) {
|
||||
dayInfo = 'YESTERDAY';
|
||||
} else if (Jiffy(createdAt).isAfter(
|
||||
now.subtract(Duration(days: 7)),
|
||||
Units.DAY,
|
||||
)) {
|
||||
dayInfo = createdAt.format('EEEE').toUpperCase();
|
||||
} else if (Jiffy(createdAt).isAfter(
|
||||
Jiffy(now).subtract(years: 1),
|
||||
Units.DAY,
|
||||
)) {
|
||||
dayInfo = createdAt.format('dd/MM').toUpperCase();
|
||||
} else {
|
||||
dayInfo = createdAt.format('dd/MM/yyyy').toUpperCase();
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
if (onMessageTap != null) {
|
||||
onMessageTap(message);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
divider,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
message.text,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.headline6
|
||||
.color
|
||||
.withOpacity(.5),
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: dayInfo,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
TextSpan(text: ' AT'),
|
||||
TextSpan(text: ' $hourInfo'),
|
||||
],
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Theme.of(context)
|
||||
.textTheme
|
||||
.headline6
|
||||
.color
|
||||
.withOpacity(.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
divider,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
|
||||
import 'back_button.dart';
|
||||
import 'channel_name.dart';
|
||||
|
||||
/// 
|
||||
/// 
|
||||
///
|
||||
/// It shows the current thread information.
|
||||
///
|
||||
/// ```dart
|
||||
/// class ThreadPage extends StatelessWidget {
|
||||
/// final Message parent;
|
||||
///
|
||||
/// ThreadPage({
|
||||
/// Key key,
|
||||
/// this.parent,
|
||||
/// }) : super(key: key);
|
||||
///
|
||||
/// @override
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return Scaffold(
|
||||
/// appBar: ThreadHeader(
|
||||
/// parent: parent,
|
||||
/// ),
|
||||
/// body: Column(
|
||||
/// children: <Widget>[
|
||||
/// Expanded(
|
||||
/// child: MessageListView(
|
||||
/// parentMessage: parent,
|
||||
/// ),
|
||||
/// ),
|
||||
/// MessageInput(
|
||||
/// parentMessage: parent,
|
||||
/// ),
|
||||
/// ],
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Usually you would use this widget as an [AppBar] inside a [Scaffold].
|
||||
/// However you can also use it as a normal widget.
|
||||
///
|
||||
/// Make sure to have a [StreamChannel] ancestor in order to provide the information about the channel.
|
||||
/// Every part of the widget uses a [StreamBuilder] to render the channel information as soon as it updates.
|
||||
///
|
||||
/// By default the widget shows a backButton that calls [Navigator.pop].
|
||||
/// You can disable this button using the [showBackButton] property of just override the behaviour
|
||||
/// with [onBackPressed].
|
||||
///
|
||||
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property.
|
||||
/// Modify it to change the widget appearance.
|
||||
class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
/// True if this header shows the leading back button
|
||||
final bool showBackButton;
|
||||
|
||||
/// Callback to call when pressing the back button.
|
||||
/// By default it calls [Navigator.pop]
|
||||
final VoidCallback onBackPressed;
|
||||
|
||||
/// The message parent of this thread
|
||||
final Message parent;
|
||||
|
||||
/// Instantiate a new ThreadHeader
|
||||
ThreadHeader({
|
||||
Key key,
|
||||
@required this.parent,
|
||||
this.showBackButton = true,
|
||||
this.onBackPressed,
|
||||
}) : preferredSize = Size.fromHeight(kToolbarHeight),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
elevation: 1,
|
||||
leading: showBackButton
|
||||
? StreamBackButton(
|
||||
onPressed: onBackPressed,
|
||||
showUnreads: true,
|
||||
)
|
||||
: SizedBox(),
|
||||
backgroundColor:
|
||||
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
|
||||
centerTitle: true,
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Thread Reply',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.title,
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'with ',
|
||||
style: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.lastMessageAt,
|
||||
),
|
||||
Flexible(
|
||||
child: ChannelName(
|
||||
textStyle: StreamChatTheme.of(context)
|
||||
.channelTheme
|
||||
.channelHeaderTheme
|
||||
.lastMessageAt,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
final Size preferredSize;
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_channel.dart';
|
||||
|
||||
/// Widget to show the current list of typing users
|
||||
class TypingIndicator extends StatelessWidget {
|
||||
/// Instantiate a new TypingIndicator
|
||||
const TypingIndicator({
|
||||
Key key,
|
||||
this.channel,
|
||||
this.alternativeWidget,
|
||||
this.style,
|
||||
this.alignment = Alignment.centerLeft,
|
||||
this.padding = const EdgeInsets.all(0),
|
||||
}) : super(key: key);
|
||||
|
||||
/// Style of the text widget
|
||||
final TextStyle style;
|
||||
|
||||
/// List of typing users
|
||||
final Channel channel;
|
||||
|
||||
/// Widget built when no typings is happening
|
||||
final Widget alternativeWidget;
|
||||
|
||||
/// The padding of this widget
|
||||
final EdgeInsets padding;
|
||||
|
||||
final Alignment alignment;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final channelState =
|
||||
channel?.state ?? StreamChannel.of(context).channel.state;
|
||||
return StreamBuilder<List<User>>(
|
||||
initialData: channelState.typingEvents,
|
||||
stream: channelState.typingEventsStream,
|
||||
builder: (context, snapshot) {
|
||||
return AnimatedSwitcher(
|
||||
duration: Duration(milliseconds: 300),
|
||||
child: snapshot.data?.isNotEmpty == true
|
||||
? Padding(
|
||||
padding: padding,
|
||||
child: Align(
|
||||
key: Key('typings'),
|
||||
alignment: alignment,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Lottie.asset(
|
||||
'animations/typing_dots.json',
|
||||
package: 'stream_chat_flutter',
|
||||
height: 4,
|
||||
),
|
||||
Text(
|
||||
' ${snapshot.data.map((u) => u.name).join(',')} ${snapshot.data.length == 1 ? 'is' : 'are'} typing',
|
||||
maxLines: 1,
|
||||
style: style,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
: Align(
|
||||
key: Key('alternative'),
|
||||
alignment: alignment,
|
||||
child: Container(
|
||||
child: alternativeWidget ?? Offstage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
class UnreadIndicator extends StatelessWidget {
|
||||
const UnreadIndicator({
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final client = StreamChat.of(context).client;
|
||||
return StreamBuilder<int>(
|
||||
stream: client.state.totalUnreadCountStream,
|
||||
initialData: client.state.totalUnreadCount,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData || snapshot.data == 0) {
|
||||
return SizedBox();
|
||||
}
|
||||
return Material(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: StreamChatTheme.of(context)
|
||||
.channelPreviewTheme
|
||||
.unreadCounterColor,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 5.0,
|
||||
right: 5.0,
|
||||
top: 2,
|
||||
bottom: 1,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'${snapshot.data}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/utils.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
class UrlAttachment extends StatelessWidget {
|
||||
final Attachment urlAttachment;
|
||||
final String hostDisplayName;
|
||||
final EdgeInsets textPadding;
|
||||
|
||||
UrlAttachment({
|
||||
@required this.urlAttachment,
|
||||
@required this.hostDisplayName,
|
||||
@required this.textPadding,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => launchURL(
|
||||
context,
|
||||
urlAttachment.ogScrapeUrl,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (urlAttachment.imageUrl != null)
|
||||
SizedBox(
|
||||
height: 16.0,
|
||||
),
|
||||
if (urlAttachment.imageUrl != null)
|
||||
Container(
|
||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
||||
margin: EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Stack(
|
||||
children: [
|
||||
Center(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: urlAttachment.imageUrl,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0.0,
|
||||
bottom: -1,
|
||||
child: Container(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 8.0,
|
||||
left: 8.0,
|
||||
right: 8.0,
|
||||
),
|
||||
child: Text(
|
||||
hostDisplayName,
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.bodyBold
|
||||
.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(16.0),
|
||||
),
|
||||
color: StreamChatTheme.of(context).colorTheme.blueAlice,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: textPadding,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (urlAttachment.title != null)
|
||||
Text(
|
||||
urlAttachment.title.trim(),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.body
|
||||
.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
if (urlAttachment.text != null)
|
||||
Text(
|
||||
urlAttachment.text,
|
||||
style: StreamChatTheme.of(context)
|
||||
.textTheme
|
||||
.body
|
||||
.copyWith(fontWeight: FontWeight.w400),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
|
||||
class UserAvatar extends StatelessWidget {
|
||||
const UserAvatar({
|
||||
Key key,
|
||||
@required this.user,
|
||||
this.constraints,
|
||||
this.onlineIndicatorConstraints,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.showOnlineStatus = true,
|
||||
this.borderRadius,
|
||||
this.onlineIndicatorAlignment = Alignment.topRight,
|
||||
this.selected = false,
|
||||
this.selectionColor,
|
||||
this.selectionThickness = 4,
|
||||
}) : super(key: key);
|
||||
|
||||
final User user;
|
||||
final Alignment onlineIndicatorAlignment;
|
||||
final BoxConstraints constraints;
|
||||
final BorderRadius borderRadius;
|
||||
final BoxConstraints onlineIndicatorConstraints;
|
||||
final void Function(User) onTap;
|
||||
final void Function(User) onLongPress;
|
||||
final bool showOnlineStatus;
|
||||
final bool selected;
|
||||
final Color selectionColor;
|
||||
final double selectionThickness;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final hasImage = user.extraData?.containsKey('image') == true &&
|
||||
user.extraData['image'] != null &&
|
||||
user.extraData['image'] != '';
|
||||
final streamChatTheme = StreamChatTheme.of(context);
|
||||
|
||||
Widget avatar = ClipRRect(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
borderRadius: borderRadius ??
|
||||
streamChatTheme.ownMessageTheme.avatarTheme.borderRadius,
|
||||
child: Container(
|
||||
constraints: constraints ??
|
||||
streamChatTheme.ownMessageTheme.avatarTheme.constraints,
|
||||
decoration: BoxDecoration(
|
||||
color: streamChatTheme.colorTheme.accentBlue,
|
||||
),
|
||||
child: hasImage
|
||||
? CachedNetworkImage(
|
||||
filterQuality: FilterQuality.high,
|
||||
imageUrl: user.extraData['image'],
|
||||
errorWidget: (_, __, ___) {
|
||||
return streamChatTheme.defaultUserImage(context, user);
|
||||
},
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: streamChatTheme.defaultUserImage(context, user),
|
||||
),
|
||||
);
|
||||
|
||||
if (selected) {
|
||||
avatar = ClipRRect(
|
||||
borderRadius: (borderRadius ??
|
||||
streamChatTheme.ownMessageTheme.avatarTheme.borderRadius) +
|
||||
BorderRadius.circular(selectionThickness),
|
||||
child: Container(
|
||||
constraints: constraints ??
|
||||
streamChatTheme.ownMessageTheme.avatarTheme.constraints,
|
||||
color: selectionColor ??
|
||||
StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(selectionThickness),
|
||||
child: avatar,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return GestureDetector(
|
||||
onTap: onTap != null ? () => onTap(user) : null,
|
||||
onLongPress: onLongPress != null ? () => onLongPress(user) : null,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
avatar,
|
||||
if (showOnlineStatus && user.online == true)
|
||||
Positioned.fill(
|
||||
child: Align(
|
||||
alignment: onlineIndicatorAlignment,
|
||||
child: Material(
|
||||
type: MaterialType.circle,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(2.0),
|
||||
constraints: onlineIndicatorConstraints ??
|
||||
BoxConstraints.tightFor(
|
||||
width: 12,
|
||||
height: 12,
|
||||
),
|
||||
child: Material(
|
||||
shape: CircleBorder(),
|
||||
color: streamChatTheme.colorTheme.accentGreen,
|
||||
),
|
||||
),
|
||||
color: StreamChatTheme.of(context).colorTheme.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||
import 'package:stream_chat_flutter/src/user_list_view.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
import 'stream_chat_theme.dart';
|
||||
|
||||
///
|
||||
/// It shows the current [User] preview.
|
||||
///
|
||||
/// The widget uses a [StreamBuilder] to render the user information image as soon as it updates.
|
||||
///
|
||||
/// Usually you don't use this widget as it's the default user preview used by [UserListView].
|
||||
///
|
||||
/// The widget renders the ui based on the first ancestor of type [StreamChatTheme].
|
||||
/// Modify it to change the widget appearance.
|
||||
class UserItem extends StatelessWidget {
|
||||
/// Instantiate a new UserItem
|
||||
const UserItem({
|
||||
Key key,
|
||||
@required this.user,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.onImageTap,
|
||||
this.selected = false,
|
||||
this.showLastOnline = true,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Function called when tapping this widget
|
||||
final void Function(User) onTap;
|
||||
|
||||
/// Function called when long pressing this widget
|
||||
final void Function(User) onLongPress;
|
||||
|
||||
/// User displayed
|
||||
final User user;
|
||||
|
||||
/// The function called when the image is tapped
|
||||
final void Function(User) onImageTap;
|
||||
|
||||
/// If true the [UserItem] will show a trailing checkmark
|
||||
final bool selected;
|
||||
|
||||
/// If true the [UserItem] will show the last seen
|
||||
final bool showLastOnline;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
onTap: () {
|
||||
if (onTap != null) {
|
||||
onTap(user);
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
if (onLongPress != null) {
|
||||
onLongPress(user);
|
||||
}
|
||||
},
|
||||
leading: UserAvatar(
|
||||
user: user,
|
||||
showOnlineStatus: true,
|
||||
onTap: (user) {
|
||||
if (onImageTap != null) {
|
||||
onImageTap(user);
|
||||
}
|
||||
},
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 40,
|
||||
width: 40,
|
||||
),
|
||||
),
|
||||
trailing: selected
|
||||
? StreamSvgIcon.checkSend(
|
||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
||||
)
|
||||
: null,
|
||||
title: Text(
|
||||
user.name,
|
||||
style: StreamChatTheme.of(context).textTheme.bodyBold,
|
||||
),
|
||||
subtitle: showLastOnline ? _buildLastActive(context) : null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLastActive(context) {
|
||||
return Text(
|
||||
user.online == true
|
||||
? 'Online'
|
||||
: 'Last online ${Jiffy(user.lastActive).fromNow()}',
|
||||
style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
|
||||
color: StreamChatTheme.of(context).colorTheme.black.withOpacity(.5)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,548 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart';
|
||||
import 'package:stream_chat_flutter/src/users_bloc.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
import 'user_item.dart';
|
||||
|
||||
/// Callback called when tapping on a user
|
||||
typedef UserTapCallback = void Function(User, Widget);
|
||||
|
||||
/// Builder used to create a custom [ListUserItem] from a [User]
|
||||
typedef UserItemBuilder = Widget Function(BuildContext, User, bool);
|
||||
|
||||
///
|
||||
/// It shows the list of current users.
|
||||
///
|
||||
/// ```dart
|
||||
/// class UsersListPage extends StatelessWidget {
|
||||
/// @override
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return Scaffold(
|
||||
/// body: UsersListView(
|
||||
/// filter: {
|
||||
/// 'members': {
|
||||
/// '\$in': [StreamChat.of(context).user.id],
|
||||
/// }
|
||||
/// },
|
||||
/// sort: [SortOption('last_message_at')],
|
||||
/// pagination: PaginationParams(
|
||||
/// limit: 20,
|
||||
/// ),
|
||||
/// channelWidget: ChannelPage(),
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
/// Make sure to have a [UsersBloc] ancestor in order to provide the information about the users.
|
||||
/// The widget uses a [ListView.separated], [GridView.builder] to render the list, grid of channels.
|
||||
///
|
||||
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme].
|
||||
/// Modify it to change the widget appearance.
|
||||
class UserListView extends StatefulWidget {
|
||||
/// Instantiate a new UserListView
|
||||
const UserListView({
|
||||
Key key,
|
||||
this.errorBuilder,
|
||||
this.emptyBuilder,
|
||||
this.filter,
|
||||
this.options,
|
||||
this.sort,
|
||||
this.pagination,
|
||||
this.onUserTap,
|
||||
this.onUserLongPress,
|
||||
this.userWidget,
|
||||
this.userItemBuilder,
|
||||
this.separatorBuilder,
|
||||
this.onImageTap,
|
||||
this.selectedUsers,
|
||||
this.pullToRefresh = true,
|
||||
this.groupAlphabetically = false,
|
||||
this.crossAxisCount = 1,
|
||||
}) : assert(
|
||||
crossAxisCount == 1 || groupAlphabetically == false,
|
||||
'Cannot group alphabetically when crossAxisCount > 1',
|
||||
),
|
||||
super(key: key);
|
||||
|
||||
/// The builder that will be used in case of error
|
||||
final Widget Function(Error error) errorBuilder;
|
||||
|
||||
/// The builder used when the channel list is empty.
|
||||
final WidgetBuilder emptyBuilder;
|
||||
|
||||
/// The query filters to use.
|
||||
/// You can query on any of the custom fields you've defined on the [Channel].
|
||||
/// You can also filter other built-in channel fields.
|
||||
final Map<String, dynamic> filter;
|
||||
|
||||
/// Query channels options.
|
||||
///
|
||||
/// state: if true returns the Channel state
|
||||
/// watch: if true listen to changes to this Channel in real time.
|
||||
final Map<String, dynamic> options;
|
||||
|
||||
/// The sorting used for the channels matching the filters.
|
||||
/// Sorting is based on field and direction, multiple sorting options can be provided.
|
||||
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
|
||||
/// Direction can be ascending or descending.
|
||||
final List<SortOption> sort;
|
||||
|
||||
/// Pagination parameters
|
||||
/// limit: the number of users to return (max is 30)
|
||||
/// offset: the offset (max is 1000)
|
||||
/// message_limit: how many messages should be included to each channel
|
||||
final PaginationParams pagination;
|
||||
|
||||
/// Function called when tapping on a channel
|
||||
/// By default it calls [Navigator.push] building a [MaterialPageRoute]
|
||||
/// with the widget [userWidget] as child.
|
||||
final UserTapCallback onUserTap;
|
||||
|
||||
/// Function called when long pressing on a channel
|
||||
final Function(User) onUserLongPress;
|
||||
|
||||
/// Widget used when opening a channel
|
||||
final Widget userWidget;
|
||||
|
||||
/// Builder used to create a custom user preview
|
||||
final UserItemBuilder userItemBuilder;
|
||||
|
||||
/// Builder used to create a custom item separator
|
||||
final Function(BuildContext, int) separatorBuilder;
|
||||
|
||||
/// The function called when the image is tapped
|
||||
final Function(User) onImageTap;
|
||||
|
||||
/// Set it to false to disable the pull-to-refresh widget
|
||||
final bool pullToRefresh;
|
||||
|
||||
/// Sets a blue trailing checkMark in [ListUserItem] for all the [selectedUsers]
|
||||
final Set<User> selectedUsers;
|
||||
|
||||
/// Set it to true to group users by their first character
|
||||
///
|
||||
/// defaults to false
|
||||
final bool groupAlphabetically;
|
||||
|
||||
/// The number of children in the cross axis.
|
||||
final int crossAxisCount;
|
||||
|
||||
@override
|
||||
_UserListViewState createState() => _UserListViewState();
|
||||
}
|
||||
|
||||
class _UserListViewState extends State<UserListView>
|
||||
with WidgetsBindingObserver {
|
||||
bool get _isListView => widget.crossAxisCount == 1;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final usersBloc = UsersBloc.of(context);
|
||||
usersBloc.queryUsers(
|
||||
filter: widget.filter,
|
||||
sort: widget.sort,
|
||||
pagination: widget.pagination,
|
||||
options: widget.options,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final usersBloc = UsersBloc.of(context);
|
||||
|
||||
if (!widget.pullToRefresh) {
|
||||
return _buildListView(usersBloc);
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
return usersBloc.queryUsers(
|
||||
filter: widget.filter,
|
||||
sort: widget.sort,
|
||||
options: widget.options,
|
||||
pagination: widget.pagination,
|
||||
);
|
||||
},
|
||||
child: _buildListView(usersBloc),
|
||||
);
|
||||
}
|
||||
|
||||
bool get isListAlreadySorted =>
|
||||
widget.sort?.any((e) => e.field == 'name' && e.direction == 1) ?? false;
|
||||
|
||||
Stream<List<ListItem>> _buildUserStream(
|
||||
UsersBlocState usersBlocState,
|
||||
) {
|
||||
return usersBlocState.usersStream.map(
|
||||
(users) {
|
||||
if (widget.groupAlphabetically) {
|
||||
var temp = users;
|
||||
if (!isListAlreadySorted) {
|
||||
temp = users..sort((curr, next) => curr.name.compareTo(next.name));
|
||||
}
|
||||
final groupedUsers = <String, List<User>>{};
|
||||
for (var e in temp) {
|
||||
final alphabet = e.name[0]?.toUpperCase();
|
||||
groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e];
|
||||
}
|
||||
final items = <ListItem>[];
|
||||
for (var key in groupedUsers.keys) {
|
||||
items.add(ListHeaderItem(key));
|
||||
items.addAll(groupedUsers[key].map((e) => ListUserItem(e)));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
return users.map((e) => ListUserItem(e)).toList();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
StreamBuilder<List<ListItem>> _buildListView(
|
||||
UsersBlocState usersBlocState,
|
||||
) {
|
||||
return StreamBuilder(
|
||||
stream: _buildUserStream(usersBlocState),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
if (snapshot.error is Error) {
|
||||
print((snapshot.error as Error).stackTrace);
|
||||
}
|
||||
|
||||
if (widget.errorBuilder != null) {
|
||||
return widget.errorBuilder(snapshot.error);
|
||||
}
|
||||
|
||||
var message = snapshot.error.toString();
|
||||
if (snapshot.error is DioError) {
|
||||
final dioError = snapshot.error as DioError;
|
||||
if (dioError.type == DioErrorType.RESPONSE) {
|
||||
message = dioError.message;
|
||||
} else {
|
||||
message = 'Check your connection and retry';
|
||||
}
|
||||
}
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
WidgetSpan(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 2.0,
|
||||
),
|
||||
child: Icon(Icons.error_outline),
|
||||
),
|
||||
),
|
||||
TextSpan(text: 'Error loading channels'),
|
||||
],
|
||||
),
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 16.0,
|
||||
),
|
||||
child: Text(message),
|
||||
),
|
||||
FlatButton(
|
||||
onPressed: () {
|
||||
usersBlocState.queryUsers(
|
||||
filter: widget.filter,
|
||||
sort: widget.sort,
|
||||
pagination: widget.pagination,
|
||||
options: widget.options,
|
||||
);
|
||||
},
|
||||
child: Text('Retry'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!snapshot.hasData) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, viewportConstraints) {
|
||||
return SingleChildScrollView(
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: viewportConstraints.maxHeight,
|
||||
),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final items = snapshot.data;
|
||||
|
||||
if (items.isEmpty && widget.emptyBuilder != null) {
|
||||
return widget.emptyBuilder(context);
|
||||
}
|
||||
|
||||
if (items.isEmpty && widget.emptyBuilder == null) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, viewportConstraints) {
|
||||
return SingleChildScrollView(
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: viewportConstraints.maxHeight,
|
||||
),
|
||||
child: Center(
|
||||
child: Text('There are no users currently'),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final child = _isListView
|
||||
? ListView.separated(
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
itemCount: items.isNotEmpty ? items.length + 1 : items.length,
|
||||
separatorBuilder: (_, index) {
|
||||
if (widget.separatorBuilder != null) {
|
||||
return widget.separatorBuilder(context, index);
|
||||
}
|
||||
return _separatorBuilder(context, index);
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
return _listItemBuilder(context, index, items);
|
||||
},
|
||||
)
|
||||
: GridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: widget.crossAxisCount,
|
||||
),
|
||||
itemCount: items.isNotEmpty ? items.length + 1 : items.length,
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
return _gridItemBuilder(context, index, items);
|
||||
},
|
||||
);
|
||||
|
||||
return LazyLoadScrollView(
|
||||
onEndOfPage: () async {
|
||||
return _listenUserPagination(usersBlocState);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _listItemBuilder(BuildContext context, int i, List<ListItem> items) {
|
||||
final usersProvider = UsersBloc.of(context);
|
||||
if (i < items.length) {
|
||||
final item = items[i];
|
||||
return item.when(
|
||||
headerItem: (header) {
|
||||
return Container(
|
||||
key: ValueKey<String>('HEADER-$header'),
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.05),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6),
|
||||
child: Text(
|
||||
header,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14.5,
|
||||
color: StreamChatTheme.of(context).colorTheme.grey,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
userItem: (user) {
|
||||
final selected = widget.selectedUsers?.contains(user) ?? false;
|
||||
return Container(
|
||||
key: ValueKey<String>('USER-${user.id}'),
|
||||
child: widget.userItemBuilder != null
|
||||
? widget.userItemBuilder(context, user, selected)
|
||||
: UserItem(
|
||||
user: user,
|
||||
onTap: (user) => widget.onUserTap(user, widget.userWidget),
|
||||
onLongPress: widget.onUserLongPress,
|
||||
onImageTap: widget.onImageTap,
|
||||
selected: selected,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return _buildQueryProgressIndicator(context, usersProvider);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _gridItemBuilder(BuildContext context, int i, List<ListItem> items) {
|
||||
final usersProvider = UsersBloc.of(context);
|
||||
if (i < items.length) {
|
||||
final item = items[i];
|
||||
return item.when(
|
||||
headerItem: (_) => Offstage(),
|
||||
userItem: (user) {
|
||||
final selected = widget.selectedUsers?.contains(user) ?? false;
|
||||
return Container(
|
||||
key: ValueKey<String>('USER-${user.id}'),
|
||||
child: widget.userItemBuilder != null
|
||||
? widget.userItemBuilder(context, user, selected)
|
||||
: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
UserAvatar(
|
||||
user: user,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
selected: selected,
|
||||
constraints: BoxConstraints.tightFor(
|
||||
height: 64,
|
||||
width: 64,
|
||||
),
|
||||
onTap: (user) =>
|
||||
widget.onUserTap(user, widget.userWidget),
|
||||
onLongPress: widget.onUserLongPress,
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Text(
|
||||
user.name,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return _buildQueryProgressIndicator(context, usersProvider);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildQueryProgressIndicator(context, UsersBlocState usersProvider) {
|
||||
return StreamBuilder<bool>(
|
||||
stream: usersProvider.queryUsersLoading,
|
||||
initialData: false,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Container(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.accentRed
|
||||
.withOpacity(.2),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: Center(
|
||||
child: Text('Error loading users'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
height: 100,
|
||||
padding: EdgeInsets.all(32),
|
||||
child: Center(
|
||||
child: snapshot.data ? CircularProgressIndicator() : Container(),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Widget _separatorBuilder(context, i) {
|
||||
return Container(
|
||||
height: 1,
|
||||
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
|
||||
);
|
||||
}
|
||||
|
||||
void _listenUserPagination(UsersBlocState usersProvider) {
|
||||
usersProvider.queryUsers(
|
||||
filter: widget.filter,
|
||||
sort: widget.sort,
|
||||
pagination: widget.pagination.copyWith(
|
||||
offset: usersProvider.users?.length ?? 0,
|
||||
),
|
||||
options: widget.options,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(UserListView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.filter?.toString() != oldWidget.filter?.toString() ||
|
||||
jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) ||
|
||||
widget.pagination?.toJson()?.toString() !=
|
||||
oldWidget.pagination?.toJson()?.toString() ||
|
||||
widget.options?.toString() != oldWidget.options?.toString()) {
|
||||
final usersBloc = UsersBloc.of(context);
|
||||
usersBloc.queryUsers(
|
||||
filter: widget.filter,
|
||||
sort: widget.sort,
|
||||
pagination: widget.pagination,
|
||||
options: widget.options,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ListItem {
|
||||
String get key {
|
||||
if (this is ListHeaderItem) {
|
||||
final header = (this as ListHeaderItem).heading;
|
||||
return 'HEADER-$header';
|
||||
}
|
||||
if (this is ListUserItem) {
|
||||
final user = (this as ListUserItem).user;
|
||||
return 'USER-${user.id}';
|
||||
}
|
||||
}
|
||||
|
||||
Widget when({
|
||||
@required Widget Function(String heading) headerItem,
|
||||
@required Widget Function(User user) userItem,
|
||||
}) {
|
||||
if (this is ListHeaderItem) {
|
||||
return headerItem((this as ListHeaderItem).heading);
|
||||
}
|
||||
if (this is ListUserItem) {
|
||||
return userItem((this as ListUserItem).user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ListHeaderItem extends ListItem {
|
||||
final String heading;
|
||||
|
||||
ListHeaderItem(this.heading);
|
||||
}
|
||||
|
||||
class ListUserItem extends ListItem {
|
||||
final User user;
|
||||
|
||||
ListUserItem(this.user);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:stream_chat_flutter/src/user_avatar.dart';
|
||||
|
||||
class UserReactionDisplay extends StatelessWidget {
|
||||
const UserReactionDisplay({
|
||||
Key key,
|
||||
@required this.reactionToEmoji,
|
||||
@required this.message,
|
||||
this.size = 30,
|
||||
}) : super(key: key);
|
||||
|
||||
final Map<String, String> reactionToEmoji;
|
||||
final Message message;
|
||||
final double size;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Colors.black87,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: reactionToEmoji.keys.map((reactionType) {
|
||||
var firstUserReaction = message.latestReactions.firstWhere(
|
||||
(element) => element.type == reactionType, orElse: () {
|
||||
return null;
|
||||
});
|
||||
|
||||
if (firstUserReaction == null) {
|
||||
return IconButton(
|
||||
iconSize: size,
|
||||
icon: Container(),
|
||||
onPressed: null,
|
||||
);
|
||||
}
|
||||
|
||||
return IconButton(
|
||||
iconSize: size,
|
||||
icon: UserAvatar(
|
||||
user: firstUserReaction.user,
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: size - 5,
|
||||
maxWidth: size - 5,
|
||||
),
|
||||
onTap: (user) {},
|
||||
),
|
||||
onPressed: () {},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
|
||||
import 'stream_chat.dart';
|
||||
|
||||
/// Widget dedicated to the management of a users list with pagination
|
||||
class UsersBloc extends StatefulWidget {
|
||||
/// The widget child
|
||||
final Widget child;
|
||||
|
||||
/// Instantiate a new UsersBloc
|
||||
const UsersBloc({
|
||||
Key key,
|
||||
@required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
UsersBlocState createState() => UsersBlocState();
|
||||
|
||||
/// Use this method to get the current [UsersBlocState] instance
|
||||
static UsersBlocState of(BuildContext context) {
|
||||
UsersBlocState state;
|
||||
|
||||
state = context.findAncestorStateOfType<UsersBlocState>();
|
||||
|
||||
if (state == null) {
|
||||
throw Exception('You must have a UsersBloc widget as ancestor');
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
/// The current state of the [UsersBloc]
|
||||
class UsersBlocState extends State<UsersBloc>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
/// The current users list
|
||||
List<User> get users => _usersController.value;
|
||||
|
||||
/// The current users list as a stream
|
||||
Stream<List<User>> get usersStream => _usersController.stream;
|
||||
|
||||
final BehaviorSubject<List<User>> _usersController = BehaviorSubject();
|
||||
|
||||
final BehaviorSubject<bool> _queryUsersLoadingController =
|
||||
BehaviorSubject.seeded(false);
|
||||
|
||||
/// The stream notifying the state of queryUsers call
|
||||
Stream<bool> get queryUsersLoading => _queryUsersLoadingController.stream;
|
||||
|
||||
/// Calls [Client.queryUsers] updating [queryUsersLoading] stream
|
||||
Future<void> queryUsers({
|
||||
Map<String, dynamic> filter,
|
||||
List<SortOption> sort,
|
||||
Map<String, dynamic> options,
|
||||
PaginationParams pagination,
|
||||
}) async {
|
||||
final client = StreamChat.of(context).client;
|
||||
|
||||
if (client.state?.user == null ||
|
||||
_queryUsersLoadingController.value == true) {
|
||||
return;
|
||||
}
|
||||
_queryUsersLoadingController.add(true);
|
||||
try {
|
||||
final clear = pagination == null ||
|
||||
pagination.offset == null ||
|
||||
pagination.offset == 0;
|
||||
|
||||
final oldUsers = List<User>.from(users ?? []);
|
||||
|
||||
final usersResponse = await client.queryUsers(
|
||||
filter: filter,
|
||||
sort: sort,
|
||||
options: options,
|
||||
pagination: pagination,
|
||||
);
|
||||
|
||||
if (clear) {
|
||||
_usersController.add(usersResponse.users);
|
||||
} else {
|
||||
final temp = oldUsers + usersResponse.users;
|
||||
_usersController.add(temp);
|
||||
}
|
||||
|
||||
_queryUsersLoadingController.add(false);
|
||||
} catch (err, stackTrace) {
|
||||
_queryUsersLoadingController.addError(err, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return widget.child;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_usersController.close();
|
||||
_queryUsersLoadingController.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'stream_svg_icon.dart';
|
||||
|
||||
import '../stream_chat_flutter.dart';
|
||||
|
||||
Future<void> launchURL(BuildContext context, String url) async {
|
||||
if (await canLaunch(url)) {
|
||||
await launch(url);
|
||||
} else {
|
||||
Scaffold.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Cannot launch the url'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> showConfirmationDialog(
|
||||
BuildContext context, {
|
||||
String title,
|
||||
Widget icon,
|
||||
String question,
|
||||
String okText,
|
||||
String cancelText,
|
||||
}) {
|
||||
return showModalBottomSheet(
|
||||
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
|
||||
context: context,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16.0),
|
||||
topRight: Radius.circular(16.0),
|
||||
)),
|
||||
builder: (context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 26.0,
|
||||
),
|
||||
if (icon != null) icon,
|
||||
SizedBox(
|
||||
height: 26.0,
|
||||
),
|
||||
Text(
|
||||
title,
|
||||
style: StreamChatTheme.of(context).textTheme.headlineBold,
|
||||
),
|
||||
SizedBox(
|
||||
height: 7.0,
|
||||
),
|
||||
Text(question),
|
||||
SizedBox(
|
||||
height: 36.0,
|
||||
),
|
||||
Container(
|
||||
color:
|
||||
StreamChatTheme.of(context).colorTheme.black.withOpacity(.08),
|
||||
height: 1.0,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
FlatButton(
|
||||
child: Text(
|
||||
cancelText,
|
||||
style: TextStyle(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.black
|
||||
.withOpacity(0.5),
|
||||
fontWeight: FontWeight.w400),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
FlatButton(
|
||||
child: Text(
|
||||
okText,
|
||||
style: TextStyle(
|
||||
color: Colors.red, fontWeight: FontWeight.w400),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Get random png with initials
|
||||
String getRandomPicUrl(User user) =>
|
||||
'https://getstream.io/random_png/?id=${user.id}&name=${user.name}';
|
||||
|
||||
/// Get websiteName from [hostName]
|
||||
String getWebsiteName(String hostName) {
|
||||
switch (hostName) {
|
||||
case 'reddit':
|
||||
return 'Reddit';
|
||||
case 'youtube':
|
||||
return 'Youtube';
|
||||
case 'wikipedia':
|
||||
return 'Wikipedia';
|
||||
case 'twitter':
|
||||
return 'Twitter';
|
||||
case 'facebook':
|
||||
return 'Facebook';
|
||||
case 'amazon':
|
||||
return 'Amazon';
|
||||
case 'yelp':
|
||||
return 'Yelp';
|
||||
case 'imdb':
|
||||
return 'IMDB';
|
||||
case 'pinterest':
|
||||
return 'Pinterest';
|
||||
case 'tripadvisor':
|
||||
return 'TripAdvisor';
|
||||
case 'instagram':
|
||||
return 'Instagram';
|
||||
case 'walmart':
|
||||
return 'Walmart';
|
||||
case 'craigslist':
|
||||
return 'Craigslist';
|
||||
case 'ebay':
|
||||
return 'eBay';
|
||||
case 'linkedin':
|
||||
return 'LinkedIn';
|
||||
case 'google':
|
||||
return 'Google';
|
||||
case 'apple':
|
||||
return 'Apple';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
String getSizeText(int bytes) {
|
||||
if (bytes == null) {
|
||||
return 'Size N/A';
|
||||
}
|
||||
|
||||
if (bytes <= 1000) {
|
||||
return '${bytes} bytes';
|
||||
} else if (bytes <= 100000) {
|
||||
return '${(bytes / 1000).toStringAsFixed(2)} KB';
|
||||
} else {
|
||||
return '${(bytes / 1000000).toStringAsFixed(2)} MB';
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
StreamSvgIcon getFileTypeImage(String type) {
|
||||
switch (type) {
|
||||
case '7z':
|
||||
return StreamSvgIcon.filetype_7z();
|
||||
break;
|
||||
case 'csv':
|
||||
return StreamSvgIcon.filetype_csv();
|
||||
break;
|
||||
case 'doc':
|
||||
return StreamSvgIcon.filetype_doc();
|
||||
break;
|
||||
case 'docx':
|
||||
return StreamSvgIcon.filetype_docx();
|
||||
break;
|
||||
case 'html':
|
||||
return StreamSvgIcon.filetype_html();
|
||||
break;
|
||||
case 'md':
|
||||
return StreamSvgIcon.filetype_md();
|
||||
break;
|
||||
case 'odt':
|
||||
return StreamSvgIcon.filetype_odt();
|
||||
break;
|
||||
case 'pdf':
|
||||
return StreamSvgIcon.filetype_pdf();
|
||||
break;
|
||||
case 'ppt':
|
||||
return StreamSvgIcon.filetype_ppt();
|
||||
break;
|
||||
case 'pptx':
|
||||
return StreamSvgIcon.filetype_pptx();
|
||||
break;
|
||||
case 'rar':
|
||||
return StreamSvgIcon.filetype_rar();
|
||||
break;
|
||||
case 'rtf':
|
||||
return StreamSvgIcon.filetype_rtf();
|
||||
break;
|
||||
case 'tar':
|
||||
return StreamSvgIcon.filetype_tar();
|
||||
break;
|
||||
case 'txt':
|
||||
return StreamSvgIcon.filetype_txt();
|
||||
break;
|
||||
case 'xls':
|
||||
return StreamSvgIcon.filetype_xls();
|
||||
break;
|
||||
case 'xlsx':
|
||||
return StreamSvgIcon.filetype_xlsx();
|
||||
break;
|
||||
case 'zip':
|
||||
return StreamSvgIcon.filetype_zip();
|
||||
break;
|
||||
default:
|
||||
return StreamSvgIcon.filetype_Generic();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
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/src/utils.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import 'attachment_error.dart';
|
||||
import 'attachment_title.dart';
|
||||
|
||||
class VideoAttachment extends StatefulWidget {
|
||||
final Attachment attachment;
|
||||
final MessageTheme messageTheme;
|
||||
final Size size;
|
||||
final Message message;
|
||||
final ShowMessageCallback onShowMessage;
|
||||
|
||||
VideoAttachment({
|
||||
Key key,
|
||||
@required this.attachment,
|
||||
@required this.messageTheme,
|
||||
this.message,
|
||||
this.size,
|
||||
this.onShowMessage,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_VideoAttachmentState createState() => _VideoAttachmentState();
|
||||
}
|
||||
|
||||
class _VideoAttachmentState extends State<VideoAttachment> {
|
||||
ChewieController _chewieController;
|
||||
VideoPlayerController _videoPlayerController;
|
||||
bool initialized = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!initialized) {
|
||||
return Container(
|
||||
height: widget.size?.height ?? 100,
|
||||
width: widget.size?.width ?? 100,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
_chewieController = ChewieController(
|
||||
videoPlayerController: _videoPlayerController,
|
||||
autoInitialize: true,
|
||||
showControls: false,
|
||||
aspectRatio: _videoPlayerController.value.aspectRatio,
|
||||
errorBuilder: (_, e) {
|
||||
if (widget.attachment.thumbUrl != null) {
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
height: widget.size?.height,
|
||||
width: widget.size?.width,
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
image: CachedNetworkImageProvider(
|
||||
widget.attachment.thumbUrl,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.attachment.titleLink != null)
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () =>
|
||||
launchURL(context, widget.attachment.titleLink),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return AttachmentError(
|
||||
attachment: widget.attachment,
|
||||
size: widget.size,
|
||||
);
|
||||
});
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
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: _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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_videoPlayerController =
|
||||
VideoPlayerController.network(widget.attachment.assetUrl);
|
||||
_videoPlayerController.initialize().whenComplete(() {
|
||||
setState(() {
|
||||
initialized = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_videoPlayerController?.dispose();
|
||||
_chewieController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user