Merge branch 'feature/new-ui' of github.com:GetStream/stream-chat-flutter into feat/messg-reply

 Conflicts:
	lib/src/media_list_view.dart
	lib/src/message_widget.dart
	pubspec.yaml
This commit is contained in:
Sahil Kumar
2021-01-04 19:56:21 +05:30
32 changed files with 2128 additions and 862 deletions
+21 -9
View File
@@ -5,7 +5,7 @@ import 'channel_info.dart';
import 'option_list_tile.dart';
class ChannelBottomSheet extends StatefulWidget {
VoidCallback onViewInfoTap;
final VoidCallback onViewInfoTap;
ChannelBottomSheet({this.onViewInfoTap});
@@ -129,16 +129,22 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
height: 24.0,
),
OptionListTile(
leading: StreamSvgIcon.user(
color: StreamChatTheme.of(context).colorTheme.grey,
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: StreamSvgIcon.userRemove(
color: StreamChatTheme.of(context).colorTheme.grey,
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: StreamSvgIcon.userRemove(
color: StreamChatTheme.of(context).colorTheme.grey,
),
),
title: 'Leave Group',
onTap: () async {
@@ -147,8 +153,11 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
),
if (isOwner)
OptionListTile(
leading: StreamSvgIcon.delete(
color: StreamChatTheme.of(context).colorTheme.accentRed,
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,
@@ -157,8 +166,11 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
},
),
OptionListTile(
leading: StreamSvgIcon.close_small(
color: StreamChatTheme.of(context).colorTheme.grey,
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: StreamSvgIcon.close_small(
color: StreamChatTheme.of(context).colorTheme.grey,
),
),
title: 'Cancel',
onTap: () {
+182
View File
@@ -0,0 +1,182 @@
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 -28
View File
@@ -8,7 +8,6 @@ import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import '../stream_chat_flutter.dart';
import './channel_name.dart';
import 'channel_image.dart';
import 'chat_info_screen.dart';
import 'stream_channel.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header.png)
@@ -99,33 +98,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
padding: const EdgeInsets.only(right: 10.0),
child: Center(
child: ChannelImage(
onTap: onImageTap ??
() async {
if (channel.memberCount == 2 && channel.isDistinct) {
final currentUser = StreamChat.of(context).user;
final otherUser = channel.state.members.firstWhere(
(element) => element.user.id != currentUser.id,
orElse: () => null,
);
if (otherUser != null) {
final pop = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: ChatInfoScreen(
user: otherUser.user,
),
),
),
);
if (pop == true) {
Navigator.pop(context);
}
}
}
},
onTap: onImageTap,
),
),
),
+14 -20
View File
@@ -22,6 +22,8 @@ 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);
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_list_view.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_list_view_paint.png)
///
@@ -75,7 +77,9 @@ class ChannelListView extends StatefulWidget {
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
@@ -139,8 +143,13 @@ class ChannelListView extends StatefulWidget {
/// 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();
}
@@ -277,6 +286,7 @@ class _ChannelListViewState extends State<ChannelListView>
if (channels.isNotEmpty) {
if (widget.crossAxisCount > 1) {
child = GridView.builder(
padding: widget.padding,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: widget.crossAxisCount),
itemCount: channels.length,
@@ -288,6 +298,7 @@ class _ChannelListViewState extends State<ChannelListView>
);
} else {
child = ListView.separated(
padding: widget.padding,
physics: AlwaysScrollableScrollPhysics(),
itemCount:
channels.isNotEmpty ? channels.length + 1 : channels.length,
@@ -315,6 +326,7 @@ class _ChannelListViewState extends State<ChannelListView>
Widget _buildLoadingWidget() {
return ListView(
padding: widget.padding,
physics: AlwaysScrollableScrollPhysics(),
children: List.generate(
25,
@@ -340,9 +352,7 @@ class _ChannelListViewState extends State<ChannelListView>
highlightColor: StreamChatTheme.of(context).colorTheme.whiteSmoke,
child: Column(
children: [
SizedBox(
height: 4.0,
),
SizedBox(height: 4.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
@@ -574,23 +584,7 @@ class _ChannelListViewState extends State<ChannelListView>
return StreamChannel(
child: ChannelBottomSheet(
onViewInfoTap: () {
if (channel.memberCount == 2 &&
channel.isDistinct) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: ChatInfoScreen(
user:
channel.state.members.first.user,
),
),
),
);
}
// TODO: Add group screen
widget.onViewInfoTap(channel);
},
),
channel: channel,
+231
View File
@@ -0,0 +1,231 @@
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 -24
View File
@@ -7,7 +7,6 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import '../stream_chat_flutter.dart';
import 'channel_name.dart';
import 'channel_unread_indicator.dart';
import 'chat_info_screen.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview_paint.png)
@@ -64,29 +63,7 @@ class ChannelPreview extends StatelessWidget {
}
},
leading: ChannelImage(
onTap: onImageTap ??
() {
if (channel.memberCount == 2 && channel.isDistinct) {
final currentUser = StreamChat.of(context).user;
final otherUser = channel.state.members.firstWhere(
(element) => element.user.id != currentUser.id,
orElse: () => null,
);
if (otherUser != null) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: ChatInfoScreen(
user: otherUser.user,
),
),
),
);
}
}
},
onTap: onImageTap,
),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
-604
View File
@@ -1,604 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import '../stream_chat_flutter.dart';
/// Detail screen for a 1:1 chat correspondence
class ChatInfoScreen extends StatefulWidget {
/// User in consideration
final User user;
const ChatInfoScreen({Key key, this.user}) : super(key: key);
@override
_ChatInfoScreenState createState() => _ChatInfoScreenState();
}
class _ChatInfoScreenState extends State<ChatInfoScreen> {
@override
Widget build(BuildContext context) {
final channel = StreamChannel.of(context).channel;
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
body: ListView(
children: [
_buildUserHeader(),
SizedBox(
height: 8.0,
),
_buildOptionListTiles(),
SizedBox(
height: 8.0,
),
if ([
'admin',
'owner',
].contains(channel.state.members
.firstWhere((m) => m.userId == channel.client.state.user.id,
orElse: () => null)
?.role))
_buildDeleteListTile(),
],
),
);
}
Widget _buildUserHeader() {
return Material(
color: StreamChatTheme.of(context).colorTheme.whiteSnow,
child: SafeArea(
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: UserAvatar(
user: widget.user,
constraints: BoxConstraints(
maxWidth: 72.0,
maxHeight: 72.0,
),
borderRadius: BorderRadius.circular(36.0),
showOnlineStatus: false,
),
),
//SizedBox(height: 4.0),
Text(
widget.user.name,
style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold),
),
SizedBox(height: 7.0),
_buildConnectedTitleState(),
SizedBox(height: 15.0),
_OptionListTile(
title: '@${widget.user.id}',
trailing: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
widget.user.name,
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
fontSize: 16.0),
),
),
onTap: () {},
),
],
),
Positioned(
top: 21,
left: 16,
child: InkWell(
child: StreamSvgIcon.left(
color: StreamChatTheme.of(context).colorTheme.black,
),
onTap: () {
Navigator.of(context).pop();
},
),
),
],
),
),
);
}
Widget _buildOptionListTiles() {
var channel = StreamChannel.of(context);
return Column(
children: [
// _OptionListTile(
// title: 'Notifications',
// leading: StreamSvgIcon.Icon_notification(
// size: 24.0,
// color: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
// ),
// trailing: CupertinoSwitch(
// value: true,
// onChanged: (val) {},
// ),
// onTap: () {},
// ),
StreamBuilder<bool>(
stream: StreamChannel.of(context).channel.isMutedStream,
builder: (context, snapshot) {
return _OptionListTile(
title: 'Mute user',
leading: StreamSvgIcon.mute(
size: 23.0,
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
),
trailing: snapshot.data == null
? CircularProgressIndicator()
: CupertinoSwitch(
value: snapshot.data,
onChanged: (val) {
if (snapshot.data) {
channel.channel.unmute();
} else {
channel.channel.mute();
}
},
),
onTap: () {},
);
}),
// _OptionListTile(
// title: 'Block User',
// leading: StreamSvgIcon.Icon_user_delete(
// size: 24.0,
// color: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
// ),
// trailing: CupertinoSwitch(
// value: widget.user.banned,
// onChanged: (val) {
// if (widget.user.banned) {
// channel.channel.shadowBan(widget.user.id, {});
// } else {
// channel.channel.unbanUser(widget.user.id);
// }
// },
// ),
// onTap: () {},
// ),
_OptionListTile(
title: 'Photos & Videos',
leading: StreamSvgIcon.pictures(
size: 32.0,
color:
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
),
trailing: StreamSvgIcon.right(),
onTap: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => _MediaDisplayScreen()));
},
),
_OptionListTile(
title: 'Files',
leading: StreamSvgIcon.files(
size: 32.0,
color:
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
),
trailing: StreamSvgIcon.right(),
onTap: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => _FileDisplayScreen()));
},
),
_OptionListTile(
title: 'Shared groups',
leading: StreamSvgIcon.Icon_group(
size: 24.0,
color:
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
),
trailing: StreamSvgIcon.right(),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => _SharedGroupsScreen(
StreamChat.of(context).user, widget.user)));
},
),
],
);
}
Widget _buildDeleteListTile() {
return _OptionListTile(
title: 'Delete conversation',
leading: StreamSvgIcon.delete(
color: Colors.red,
size: 24.0,
),
onTap: () {
_showDeleteDialog();
},
titleColor: Colors.red,
);
}
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: Colors.red,
),
);
var channel = StreamChannel.of(context).channel;
if (res == true) {
await channel.delete().then((value) {
Navigator.pop(context);
});
}
}
Widget _buildConnectedTitleState() {
var alternativeWidget;
final otherMember = widget.user;
if (otherMember != null) {
if (otherMember.online) {
alternativeWidget = Text(
'Online',
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5)),
);
} else {
alternativeWidget = Text(
'Last seen ${Jiffy(otherMember.lastActive).fromNow()}',
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5)),
);
}
}
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (widget.user.online)
Material(
type: MaterialType.circle,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
constraints: BoxConstraints.tightFor(
width: 28,
height: 12,
),
child: Material(
shape: CircleBorder(),
color: StreamChatTheme.of(context).colorTheme.accentGreen,
),
),
color: StreamChatTheme.of(context).colorTheme.white,
),
alternativeWidget,
],
);
}
}
class _OptionListTile extends StatelessWidget {
final String title;
final StreamSvgIcon leading;
final Widget trailing;
final VoidCallback onTap;
final Color titleColor;
_OptionListTile({
this.title,
this.leading,
this.trailing,
this.onTap,
this.titleColor,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
color: StreamChatTheme.of(context).colorTheme.white,
height: 2.0,
),
Material(
color: StreamChatTheme.of(context).colorTheme.whiteSnow,
child: Container(
height: 56.0,
child: InkWell(
onTap: onTap,
child: Row(
children: [
if (leading != null)
Expanded(
child: Center(child: leading),
),
if (leading == null)
SizedBox(
width: 16.0,
),
Expanded(
flex: 4,
child: Text(
title,
style: TextStyle(
fontWeight: FontWeight.w600, color: titleColor),
)),
Expanded(
flex: 2,
child: Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Align(
alignment: Alignment.centerRight,
child: trailing ?? Container(),
),
),
),
],
),
),
),
),
],
);
}
}
class _SharedGroupsScreen extends StatefulWidget {
final User mainUser;
final User otherUser;
_SharedGroupsScreen(this.mainUser, this.otherUser);
@override
__SharedGroupsScreenState createState() => __SharedGroupsScreenState();
}
class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
@override
Widget build(BuildContext context) {
var chat = StreamChat.of(context);
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
appBar: AppBar(
brightness: Theme.of(context).brightness,
elevation: 1,
centerTitle: true,
title: Text(
'Shared Groups',
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: FutureBuilder<List<Channel>>(
future: chat.client.queryChannels(
filter: {
r'$and': [
{
'members': {
r'$in': [widget.otherUser.id],
},
},
{
'members': {
r'$in': [widget.mainUser.id],
},
}
],
},
),
builder: (context, snapshot) {
if (snapshot.data == null) {
return Center(
child: CircularProgressIndicator(),
);
}
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, position) {
return StreamChannel(
channel: snapshot.data[position],
child: _buildListTile(snapshot.data[position]),
);
},
);
},
),
);
}
Widget _buildListTile(Channel channel) {
var extraData = channel.extraData;
var members = channel.state.members;
var textStyle = TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold);
return Container(
height: 64.0,
child: LayoutBuilder(builder: (context, constraints) {
String title;
if (extraData['name'] == null) {
final otherMembers = members.where(
(member) => member.userId != StreamChat.of(context).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 Column(
children: [
Expanded(
child: Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: ChannelImage(
channel: channel,
constraints:
BoxConstraints(maxWidth: 40.0, maxHeight: 40.0),
),
),
Expanded(
child: Text(
title,
style: textStyle,
)),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${channel.memberCount} members',
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5)),
),
)
],
),
),
Container(
height: 1.0,
color:
StreamChatTheme.of(context).colorTheme.black.withOpacity(.08),
),
],
);
}),
);
}
}
class _MediaDisplayScreen extends StatelessWidget {
@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,
),
);
}
}
class _FileDisplayScreen extends StatelessWidget {
@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,
),
);
}
}
+9
View File
@@ -6,9 +6,12 @@ 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
@@ -18,6 +21,7 @@ class FullScreenMedia extends StatefulWidget {
final int startIndex;
final String userName;
final DateTime sentAt;
final ShowMessageCallback onShowMessage;
/// Instantiate a new FullScreenImage
const FullScreenMedia({
@@ -27,6 +31,7 @@ class FullScreenMedia extends StatefulWidget {
this.startIndex = 0,
this.userName = '',
this.sentAt,
this.onShowMessage,
}) : super(key: key);
@override
@@ -167,6 +172,10 @@ class _FullScreenMediaState extends State<FullScreenMedia>
message: widget.message,
urls: widget.mediaAttachments,
currentIndex: _currentPage,
onShowMessage: () {
widget.onShowMessage(
widget.message, StreamChannel.of(context).channel);
},
),
ImageFooter(
currentPage: _currentPage,
+4
View File
@@ -12,6 +12,7 @@ class GiphyAttachment extends StatelessWidget {
final MessageTheme messageTheme;
final Message message;
final Size size;
final ShowMessageCallback onShowMessage;
const GiphyAttachment({
Key key,
@@ -19,6 +20,7 @@ class GiphyAttachment extends StatelessWidget {
this.messageTheme,
this.message,
this.size,
this.onShowMessage,
}) : super(key: key);
@override
@@ -75,6 +77,7 @@ class GiphyAttachment extends StatelessWidget {
userName: message.user.name,
sentAt: message.createdAt,
message: message,
onShowMessage: onShowMessage,
),
);
}));
@@ -327,6 +330,7 @@ class GiphyAttachment extends StatelessWidget {
userName: message.user.name,
sentAt: message.createdAt,
message: message,
onShowMessage: onShowMessage,
),
);
}));
+9 -5
View File
@@ -15,9 +15,15 @@ class ImageActionsModal extends StatelessWidget {
final String sentAt;
final List<Attachment> urls;
final currentIndex;
final VoidCallback onShowMessage;
ImageActionsModal(
{this.message, this.userName, this.sentAt, this.urls, this.currentIndex});
{this.message,
this.userName,
this.sentAt,
this.urls,
this.currentIndex,
this.onShowMessage});
@override
Widget build(BuildContext context) {
@@ -134,10 +140,8 @@ class ImageActionsModal extends StatelessWidget {
size: 24.0,
color:
StreamChatTheme.of(context).colorTheme.black,
), () {
Navigator.pop(context);
Navigator.pop(context);
}),
),
onShowMessage),
_buildButton(
context,
'Save ${urls[currentIndex].type == 'video' ? 'Video' : 'Image'}',
+8 -2
View File
@@ -12,6 +12,8 @@ class ImageAttachment extends StatelessWidget {
final Message message;
final MessageTheme messageTheme;
final Size size;
final bool showTitle;
final ShowMessageCallback onShowMessage;
const ImageAttachment({
Key key,
@@ -19,6 +21,8 @@ class ImageAttachment extends StatelessWidget {
@required this.message,
@required this.size,
this.messageTheme,
this.showTitle = true,
this.onShowMessage,
}) : super(key: key);
@override
@@ -54,6 +58,7 @@ class ImageAttachment extends StatelessWidget {
userName: message.user.name,
sentAt: message.createdAt,
message: message,
onShowMessage: onShowMessage,
),
);
},
@@ -83,7 +88,7 @@ class ImageAttachment extends StatelessWidget {
),
),
),
if (attachment.title != null)
if (showTitle && attachment.title != null)
Material(
color: messageTheme.messageBackgroundColor,
child: AttachmentTitle(
@@ -93,7 +98,8 @@ class ImageAttachment extends StatelessWidget {
),
],
),
if (attachment.titleLink != null || attachment.ogScrapeUrl != null)
if (showTitle &&
(attachment.titleLink != null || attachment.ogScrapeUrl != null))
Positioned.fill(
child: Material(
color: Colors.transparent,
+109 -372
View File
@@ -12,11 +12,10 @@ 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.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class ImageFooter extends StatefulWidget {
class ImageFooter extends StatefulWidget implements PreferredSizeWidget {
/// Callback to call when pressing the back button.
/// By default it calls [Navigator.pop]
final VoidCallback onBackPressed;
@@ -61,12 +60,12 @@ class ImageFooter extends StatefulWidget {
class _ImageFooterState extends State<ImageFooter> {
bool _userSearchMode = false;
TextEditingController _searchController;
TextEditingController _messageController = TextEditingController();
FocusNode _messageFocusNode = FocusNode();
final TextEditingController _messageController = TextEditingController();
final FocusNode _messageFocusNode = FocusNode();
String _channelNameQuery;
List<Channel> _selectedChannels = [];
final List<Channel> _selectedChannels = [];
bool _loading = false;
Timer _debounce;
@@ -103,48 +102,67 @@ class _ImageFooterState extends State<ImageFooter> {
@override
Widget build(BuildContext context) {
return SafeArea(
child: Container(
color:
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: StreamSvgIcon.icon_SHARE(
size: 24.0,
color: StreamChatTheme.of(context).colorTheme.black,
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');
},
),
onPressed: () {
_buildShareModal(context);
},
),
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,
),
],
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,
IconButton(
icon: StreamSvgIcon.Icon_grid(
color: StreamChatTheme.of(context).colorTheme.black,
),
onPressed: () {
_buildPhotosModal(context);
},
),
onPressed: () {
_buildPhotosModal(context);
},
),
],
],
),
),
),
);
@@ -251,305 +269,26 @@ class _ImageFooterState extends State<ImageFooter> {
);
}
void _buildShareModal(context) {
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(builder: (context, modalSetState) {
modalSetStateCallback = modalSetState;
return Padding(
padding: EdgeInsets.only(
top: _userSearchMode || _messageFocusNode.hasFocus
? 16.0
: MediaQuery.of(context).size.height / 2,
left: 8.0,
right: 8.0),
child: Material(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0),
),
clipBehavior: Clip.antiAlias,
child: Scaffold(
body: UsersBloc(
child: Column(
children: [
_buildTextInputSection(modalSetState),
if (_userSearchMode)
SizedBox(
height: 22.0,
),
Expanded(
child: ChannelsBloc(
child: ChannelListView(
selectedChannels: _selectedChannels,
onChannelTap: (channel, _) {
_searchController.clear();
if (!_selectedChannels.contains(channel)) {
modalSetState(() {
_selectedChannels.add(channel);
});
} else {
modalSetState(() {
_selectedChannels.remove(channel);
});
}
},
crossAxisCount: 4,
pagination: PaginationParams(
limit: 25,
),
filter: {
if (_channelNameQuery?.trim()?.isNotEmpty == true)
'name': {
r'$autocomplete': _channelNameQuery,
},
'id': {
r'$ne': StreamChat.of(context).user.id,
},
},
sort: [
SortOption(
'name',
direction: SortOption.ASC,
),
],
emptyBuilder: (_) {
return LayoutBuilder(
builder: (context, viewportConstraints) {
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight:
viewportConstraints.maxHeight,
),
child: Center(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(24),
child: StreamSvgIcon.search(
size: 96,
color: Colors.grey,
),
),
Text(
'No chat matches these keywords...'),
],
),
),
),
);
},
);
},
),
),
),
if (_selectedChannels.isNotEmpty)
_buildShareTextInputSection(modalSetState),
if (!_userSearchMode && _selectedChannels.isEmpty)
Align(
alignment: Alignment.bottomCenter,
child: Container(
color: StreamChatTheme.of(context).colorTheme.white,
height: 48.0,
child: Material(
color:
StreamChatTheme.of(context).colorTheme.white,
child: InkWell(
onTap: () async {
var url = widget
.mediaAttachments[widget.currentPage]
.imageUrl ??
widget
.mediaAttachments[widget.currentPage]
.assetUrl ??
widget
.mediaAttachments[widget.currentPage]
.thumbUrl;
if (widget
.mediaAttachments[widget.currentPage]
.type ==
'video') {
await _saveVideo(url);
Navigator.pop(context);
} else {
await _saveImage(url);
Navigator.pop(context);
}
},
child: SizedBox.expand(
child: Center(
child: Text(
'Save to Photos',
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.accentBlue,
fontWeight: FontWeight.bold,
),
),
),
),
),
),
),
),
],
),
),
),
),
);
});
},
);
}
Widget _buildTextInputSection(modalSetState) {
if (_userSearchMode) {
return Column(
children: [
SizedBox(
height: 16.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(
width: 16.0,
),
Expanded(
child: TextField(
controller: _searchController,
cursorColor: StreamChatTheme.of(context).colorTheme.black,
autofocus: true,
decoration: InputDecoration(
isDense: true,
prefixIconConstraints:
BoxConstraints.tight(Size(36.0, 44.0)),
prefixIcon: Padding(
padding: const EdgeInsets.symmetric(
vertical: 2.0, horizontal: 6.0),
child: StreamSvgIcon.search(
color: StreamChatTheme.of(context).colorTheme.black,
),
),
hintText: 'Search',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(32.0),
borderSide: BorderSide(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.08)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(32.0),
borderSide: BorderSide(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.08)),
),
contentPadding: EdgeInsets.zero,
),
),
),
SizedBox(
width: 8.0,
),
IconButton(
icon: StreamSvgIcon.close_small(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
),
onPressed: () {
modalSetState(() {
_userSearchMode = false;
});
setState(() {});
},
)
],
),
],
);
} else {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: IconButton(
icon: StreamSvgIcon.search(
color: StreamChatTheme.of(context).colorTheme.black,
),
iconSize: 24.0,
onPressed: () {
modalSetState(() {
_userSearchMode = true;
});
},
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Select a Chat to Share',
style: StreamChatTheme.of(context).textTheme.headlineBold,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6.0),
child: IconButton(
icon: StreamSvgIcon.share_arrow(
color: StreamChatTheme.of(context).colorTheme.black,
),
onPressed: () async {
var url =
widget.mediaAttachments[widget.currentPage].imageUrl ??
widget.mediaAttachments[widget.currentPage].assetUrl ??
widget.mediaAttachments[widget.currentPage].thumbUrl;
var type =
widget.mediaAttachments[widget.currentPage].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');
},
),
),
],
);
}
}
Widget _buildShareTextInputSection(modalSetState) {
return Align(
alignment: Alignment.bottomCenter,
child: Container(
color: StreamChatTheme.of(context).colorTheme.white,
height: 56.0,
child: _loading
? Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: CircularProgressIndicator(),
),
)
: Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
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,
@@ -561,11 +300,10 @@ class _ImageFooterState extends State<ImageFooter> {
setState(() {});
},
decoration: InputDecoration(
isDense: true,
prefixText: ' ',
hintText: 'Add a comment',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(32.0),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(24.0),
borderSide: BorderSide(
color: StreamChatTheme.of(context)
.colorTheme
@@ -574,49 +312,48 @@ class _ImageFooterState extends State<ImageFooter> {
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(32.0),
borderRadius: BorderRadius.circular(24.0),
borderSide: BorderSide(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.16),
)),
contentPadding: EdgeInsets.symmetric(vertical: 12.0),
contentPadding: const EdgeInsets.all(0),
),
),
),
),
IconTheme(
data: StreamChatTheme.of(context)
.channelTheme
.messageInputButtonIconTheme,
child: Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: InkWell(
onTap: () async {
modalSetState(() {
_loading = true;
});
await sendMessage();
modalSetState(() {
_loading = false;
});
},
child: Transform.rotate(
angle: -pi / 2,
child: StreamSvgIcon.Icon_send_message(
color: StreamChatTheme.of(context)
.colorTheme
.accentBlue,
),
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,
),
),
),
),
),
],
),
],
),
),
),
);
}
+3
View File
@@ -10,11 +10,13 @@ class ImageGroup extends StatelessWidget {
@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) {
@@ -119,6 +121,7 @@ class ImageGroup extends StatelessWidget {
userName: message.user.name,
sentAt: message.createdAt,
message: message,
onShowMessage: onShowMessage,
),
),
),
+5
View File
@@ -13,6 +13,9 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
/// 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;
@@ -35,6 +38,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
this.currentIndex,
this.showBackButton = true,
this.onBackPressed,
this.onShowMessage,
this.onTitleTap,
this.onImageTap,
this.userName = '',
@@ -111,6 +115,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
message: message,
urls: urls,
currentIndex: currentIndex,
onShowMessage: onShowMessage,
),
);
});
+1 -6
View File
@@ -38,7 +38,6 @@ class _MediaListViewState extends State<MediaListView> {
final _media = <AssetEntity>[];
final ScrollController _scrollController = ScrollController();
int _currentPage = 0;
bool _endPagination = false;
@override
Widget build(BuildContext context) {
@@ -160,11 +159,7 @@ class _MediaListViewState extends State<MediaListView> {
final media = await assetList.getAssetListPaged(_currentPage, 50);
if (media.isEmpty) {
setState(() {
_endPagination = true;
});
} else {
if (!media.isEmpty) {
setState(() {
_media.addAll(media);
});
+5
View File
@@ -122,6 +122,7 @@ class MessageListView extends StatefulWidget {
this.itemPositionListener,
this.onMessageSwiped,
this.highlightInitialMessage = false,
this.onShowMessage,
}) : super(key: key);
/// Function used to build a custom message widget
@@ -174,6 +175,8 @@ class MessageListView extends StatefulWidget {
/// Also See [StreamChannel]
final bool highlightInitialMessage;
final ShowMessageCallback onShowMessage;
@override
_MessageListViewState createState() => _MessageListViewState();
}
@@ -724,6 +727,7 @@ class _MessageListViewState extends State<MessageListView> {
messageTheme: isMyMessage
? StreamChatTheme.of(context).ownMessageTheme
: StreamChatTheme.of(context).otherMessageTheme,
onShowMessage: widget.onShowMessage,
);
}
@@ -816,6 +820,7 @@ class _MessageListViewState extends State<MessageListView> {
: StreamChatTheme.of(context).otherMessageTheme,
readList: readList,
allRead: allRead,
onShowMessage: widget.onShowMessage,
);
if (!isThreadMessage) {
+2
View File
@@ -55,6 +55,7 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
/// Calls [Client.search] updating [queryMessagesLoading] stream
Future<void> search({
Map<String, dynamic> filter,
Map<String, dynamic> messageFilter,
List<SortOption> sort,
String query,
PaginationParams pagination,
@@ -78,6 +79,7 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
sort,
query,
pagination,
messageFilters: messageFilter,
);
if (clear) {
+154 -169
View File
@@ -126,6 +126,8 @@ class MessageWidget extends StatefulWidget {
final List<Read> readList;
final ShowMessageCallback onShowMessage;
/// If true show the users username next to the timestamp of the message
final bool showUsername;
final bool showTimestamp;
@@ -169,6 +171,7 @@ class MessageWidget extends StatefulWidget {
this.onUserAvatarTap,
this.onLinkTap,
this.onMessageActions,
this.onShowMessage,
this.editMessageInputBuilder,
this.textBuilder,
Map<String, AttachmentBuilder> customAttachmentBuilders,
@@ -192,6 +195,7 @@ class MessageWidget extends StatefulWidget {
MediaQuery.of(context).size.width * 0.8,
MediaQuery.of(context).size.height * 0.3,
),
onShowMessage: onShowMessage,
);
},
'video': (context, message, attachment) {
@@ -203,6 +207,7 @@ class MessageWidget extends StatefulWidget {
MediaQuery.of(context).size.height * 0.3,
),
message: message,
onShowMessage: onShowMessage,
);
},
'giphy': (context, message, attachment) {
@@ -214,6 +219,7 @@ class MessageWidget extends StatefulWidget {
MediaQuery.of(context).size.width * 0.8,
MediaQuery.of(context).size.height * 0.3,
),
onShowMessage: onShowMessage,
);
},
'file': (context, message, attachment) {
@@ -285,111 +291,115 @@ class _MessageWidgetState extends State<MessageWidget> {
widget.message.user.id == StreamChat.of(context).user.id;
return Portal(
child: Padding(
padding: widget.padding ?? EdgeInsets.all(8),
child: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: 0.75,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Stack(
clipBehavior: Clip.none,
alignment: AlignmentDirectional.bottomStart,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (widget.showUserAvatar == DisplayWidget.show)
_buildUserAvatar(),
SizedBox(width: 6),
if (widget.showUserAvatar == DisplayWidget.hide)
SizedBox(
width: widget.messageTheme.avatarTheme
.constraints.maxWidth +
8,
),
Flexible(
child: PortalEntry(
portal: Container(
transform:
Matrix4.translationValues(-16, 2, 0),
child: _buildReactionIndicator(context),
constraints:
BoxConstraints(maxWidth: 22 * 6.0),
child: GestureDetector(
onLongPress: () => onLongPress(context),
child: Padding(
padding: widget.padding ?? EdgeInsets.all(8),
child: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: 0.75,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Stack(
clipBehavior: Clip.none,
alignment: AlignmentDirectional.bottomStart,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (widget.showUserAvatar == DisplayWidget.show)
_buildUserAvatar(),
SizedBox(width: 6),
if (widget.showUserAvatar == DisplayWidget.hide)
SizedBox(
width: widget.messageTheme.avatarTheme
.constraints.maxWidth +
8,
),
portalAnchor: Alignment(-1.0, -1.0),
childAnchor: Alignment(1, -1.0),
child: Stack(
clipBehavior: Clip.none,
children: [
Padding(
padding: widget.showReactions
? EdgeInsets.only(
top: widget.message.reactionCounts
?.isNotEmpty ==
true
? 18
: 0,
)
: EdgeInsets.zero,
child: (widget.message.isDeleted &&
!isFailedState)
? Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(
widget.reverse ? pi : 0),
child: DeletedMessage(
reverse: widget.reverse,
borderRadiusGeometry:
widget.borderRadiusGeometry,
borderSide: widget.borderSide,
shape: widget.shape,
messageTheme:
widget.messageTheme,
),
)
: Material(
clipBehavior: Clip.antiAlias,
shape: widget.shape ??
RoundedRectangleBorder(
side: isOnlyEmoji
? BorderSide.none
: widget.borderSide ??
BorderSide(
color: Theme.of(context)
.brightness ==
Brightness
.dark
? StreamChatTheme.of(
context)
.colorTheme
.white
.withAlpha(
24)
: StreamChatTheme.of(
context)
.colorTheme
.black
.withAlpha(
24),
),
borderRadius: widget
.borderRadiusGeometry ??
BorderRadius.zero,
),
color: _getBackgroundColor(),
child: InkWell(
Flexible(
child: PortalEntry(
portal: Container(
transform:
Matrix4.translationValues(-16, 2, 0),
child: _buildReactionIndicator(context),
constraints:
BoxConstraints(maxWidth: 22 * 6.0),
),
portalAnchor: Alignment(-1.0, -1.0),
childAnchor: Alignment(1, -1.0),
child: Stack(
clipBehavior: Clip.none,
children: [
Padding(
padding: widget.showReactions
? EdgeInsets.only(
top: widget
.message
.reactionCounts
?.isNotEmpty ==
true
? 18
: 0,
)
: EdgeInsets.zero,
child: (widget.message.isDeleted &&
!isFailedState)
? Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(
widget.reverse ? pi : 0),
child: DeletedMessage(
reverse: widget.reverse,
borderRadiusGeometry: widget
.borderRadiusGeometry,
borderSide: widget.borderSide,
shape: widget.shape,
messageTheme:
widget.messageTheme,
),
)
: Material(
clipBehavior: Clip.antiAlias,
shape: widget.shape ??
RoundedRectangleBorder(
side: isOnlyEmoji
? BorderSide.none
: widget.borderSide ??
BorderSide(
color: Theme.of(context)
.brightness ==
Brightness
.dark
? StreamChatTheme.of(
context)
.colorTheme
.white
.withAlpha(
24)
: StreamChatTheme.of(
context)
.colorTheme
.black
.withAlpha(
24),
),
borderRadius: widget
.borderRadiusGeometry ??
BorderRadius.zero,
),
color: _getBackgroundColor(),
child: InkWell(
onLongPress: () =>
onLongPress(context),
child: Padding(
@@ -414,54 +424,55 @@ class _MessageWidgetState extends State<MessageWidget> {
_buildTextBubble(
context),
],
),
),
),
),
),
),
if (widget.showReactionPickerIndicator)
Positioned(
right: 0,
top: -6,
child: Transform(
transform: Matrix4.rotationY(
widget.reverse ? pi : 0),
child: CustomPaint(
painter: ReactionBubblePainter(
widget.messageTheme
.reactionsBackgroundColor,
widget.messageTheme
.reactionsBorderColor,
),
if (widget.showReactionPickerIndicator)
Positioned(
right: 0,
top: -6,
child: Transform(
transform: Matrix4.rotationY(
widget.reverse ? pi : 0),
child: CustomPaint(
painter: ReactionBubblePainter(
widget.messageTheme
.reactionsBackgroundColor,
widget.messageTheme
.reactionsBorderColor,
),
),
),
),
),
],
],
),
),
),
),
],
),
if (showBottomRow) SizedBox(height: 20.0),
],
),
if (showBottomRow) _buildBottomRow(leftPadding),
if (isFailedState)
Positioned(
left: widget.reverse ? -3 : null,
right: widget.reverse ? null : -9,
bottom: showBottomRow ? 20 : 0,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
],
),
child: StreamSvgIcon.error(size: 20),
),
if (showBottomRow) SizedBox(height: 20.0),
],
),
],
),
],
if (showBottomRow) _buildBottomRow(leftPadding),
if (isFailedState)
Positioned(
left: widget.reverse ? -3 : null,
right: widget.reverse ? null : -9,
bottom: showBottomRow ? 20 : 0,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: StreamSvgIcon.error(size: 20),
),
),
],
),
],
),
),
),
),
@@ -622,33 +633,6 @@ class _MessageWidgetState extends State<MessageWidget> {
widget.message.attachments?.any((element) => element.type == 'giphy') ==
true;
Widget _buildReadIndicator() {
var padding = 0.0;
return Stack(
children: widget.readList.map((e) {
padding += 10.0;
return Positioned(
left: padding - 10,
bottom: 0,
top: 0,
child: Material(
color: StreamChatTheme.of(context).colorTheme.white,
clipBehavior: Clip.antiAlias,
shape: CircleBorder(),
child: Padding(
padding: const EdgeInsets.all(1.0),
child: UserAvatar(
user: e.user,
constraints: BoxConstraints.loose(Size.fromRadius(8)),
showOnlineStatus: false,
),
),
),
);
}).toList(),
);
}
Widget _buildReactionIndicator(
BuildContext context,
) {
@@ -782,6 +766,7 @@ class _MessageWidgetState extends State<MessageWidget> {
),
images: images,
message: widget.message,
onShowMessage: widget.onShowMessage,
),
),
),
+20 -15
View File
@@ -4,10 +4,13 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
class OptionListTile extends StatelessWidget {
final String title;
final StreamSvgIcon leading;
final Widget leading;
final Widget trailing;
final VoidCallback onTap;
final Color titleColor;
final Color tileColor;
final Color separatorColor;
final TextStyle titleTextStyle;
OptionListTile({
this.title,
@@ -15,6 +18,9 @@ class OptionListTile extends StatelessWidget {
this.trailing,
this.onTap,
this.titleColor,
this.tileColor,
this.separatorColor,
this.titleTextStyle,
});
@override
@@ -22,21 +28,19 @@ class OptionListTile extends StatelessWidget {
return Column(
children: [
Container(
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
color: separatorColor ??
StreamChatTheme.of(context).colorTheme.greyGainsboro,
height: 1.0,
),
Material(
color: StreamChatTheme.of(context).colorTheme.white,
color: tileColor ?? StreamChatTheme.of(context).colorTheme.white,
child: Container(
height: 63.0,
child: InkWell(
onTap: onTap,
child: Row(
children: [
if (leading != null)
Expanded(
child: Center(child: leading),
),
if (leading != null) Center(child: leading),
if (leading == null)
SizedBox(
width: 16.0,
@@ -45,14 +49,15 @@ class OptionListTile extends StatelessWidget {
flex: 4,
child: Text(
title,
style: titleColor == null
? StreamChatTheme.of(context).textTheme.bodyBold
: StreamChatTheme.of(context)
.textTheme
.bodyBold
.copyWith(
color: titleColor,
),
style: titleTextStyle ??
(titleColor == null
? StreamChatTheme.of(context).textTheme.bodyBold
: StreamChatTheme.of(context)
.textTheme
.bodyBold
.copyWith(
color: titleColor,
)),
),
),
Expanded(
+5 -5
View File
@@ -136,11 +136,11 @@ class _ReactionPickerState extends State<ReactionPicker>
/// Add a reaction to the message
void sendReaction(BuildContext context, String reactionType) {
StreamChannel.of(context)
.channel
.sendReaction(widget.message, reactionType, extraData: {
'enforce_unique': true,
});
StreamChannel.of(context).channel.sendReaction(
widget.message,
reactionType,
enforceUnique: true,
);
pop();
}
-1
View File
@@ -112,7 +112,6 @@ class StreamChatState extends State<StreamChat> with WidgetsBindingObserver {
inputBackground: themeData?.channelTheme?.inputBackground,
messageInputButtonIconTheme:
themeData?.channelTheme?.messageInputButtonIconTheme,
inputGradient: themeData?.channelTheme?.inputGradient,
messageInputButtonTheme:
themeData?.channelTheme?.messageInputButtonTheme,
),
+5 -23
View File
@@ -92,12 +92,7 @@ class StreamChatThemeData {
return defaultTheme.copyWith(
primaryIconTheme: theme.primaryIconTheme,
channelTheme: defaultTheme.channelTheme.copyWith(
inputGradient: LinearGradient(colors: [
theme.accentColor.withOpacity(.5),
theme.accentColor,
]),
),
channelTheme: defaultTheme.channelTheme,
ownMessageTheme: defaultTheme.ownMessageTheme.copyWith(
replies: defaultTheme.ownMessageTheme.replies.copyWith(
color: theme.accentColor,
@@ -168,8 +163,6 @@ class StreamChatThemeData {
this.channelTheme.messageInputButtonIconTheme,
messageInputButtonTheme: channelTheme.messageInputButtonTheme ??
this.channelTheme.messageInputButtonTheme,
inputGradient:
channelTheme.inputGradient ?? this.channelTheme.inputGradient,
inputBackground: channelTheme.inputBackground ??
this.channelTheme.inputBackground,
) ??
@@ -273,10 +266,6 @@ class StreamChatThemeData {
),
),
inputBackground: colorTheme.white.withAlpha(12),
inputGradient: LinearGradient(colors: [
Color(0xFF00AEFF),
Color(0xFF0076FF),
]),
),
ownMessageTheme: MessageTheme(
messageText: TextStyle(
@@ -342,19 +331,19 @@ class StreamChatThemeData {
assetName: 'Icon_love_reaction.svg',
),
ReactionIcon(
type: 'thumbs_up',
type: 'like',
assetName: 'Icon_thumbs_up_reaction.svg',
),
ReactionIcon(
type: 'thumbs_down',
type: 'sad',
assetName: 'Icon_thumbs_down_reaction.svg',
),
ReactionIcon(
type: 'lol',
type: 'haha',
assetName: 'Icon_LOL_reaction.svg',
),
ReactionIcon(
type: 'wut',
type: 'wow',
assetName: 'Icon_wut_reaction.svg',
),
],
@@ -597,9 +586,6 @@ class ChannelTheme {
/// Theme of the send button in [MessageInput]
final ButtonThemeData messageInputButtonTheme;
/// Gradient of [MessageInput]
final Gradient inputGradient;
/// Background color of [MessageInput]
final Color inputBackground;
@@ -608,7 +594,6 @@ class ChannelTheme {
this.messageInputButtonIconTheme,
this.messageInputButtonTheme,
this.inputBackground,
this.inputGradient,
});
/// Creates a copy of [ChannelTheme] with specified attributes overridden.
@@ -616,7 +601,6 @@ class ChannelTheme {
ChannelHeaderTheme channelHeaderTheme,
IconThemeData messageInputButtonIconTheme,
ButtonThemeData messageInputButtonTheme,
Gradient inputGradient,
Color inputBackground,
}) =>
ChannelTheme(
@@ -633,8 +617,6 @@ class ChannelTheme {
messageInputButtonIconTheme ?? this.messageInputButtonIconTheme,
messageInputButtonTheme:
messageInputButtonTheme ?? this.messageInputButtonTheme,
inputGradient: inputGradient ?? this.inputGradient,
inputBackground: inputBackground ?? this.inputBackground,
);
}
+12
View File
@@ -793,4 +793,16 @@ class StreamSvgIcon extends StatelessWidget {
height: size,
);
}
factory StreamSvgIcon.Icon_user_settings({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_user_settings.svg',
color: color,
width: size,
height: size,
);
}
}
+3
View File
@@ -14,6 +14,7 @@ class VideoAttachment extends StatefulWidget {
final MessageTheme messageTheme;
final Size size;
final Message message;
final ShowMessageCallback onShowMessage;
VideoAttachment({
Key key,
@@ -21,6 +22,7 @@ class VideoAttachment extends StatefulWidget {
@required this.messageTheme,
this.message,
this.size,
this.onShowMessage,
}) : super(key: key);
@override
@@ -95,6 +97,7 @@ class _VideoAttachmentState extends State<VideoAttachment> {
userName: widget.message.user.name,
sentAt: widget.message.createdAt,
message: widget.message,
onShowMessage: widget.onShowMessage,
),
),
),