Merge pull request #177 from GetStream/feature/group-info-screen

Group Info Screen
This commit is contained in:
Salvatore Giordano
2021-01-04 14:22:05 +01:00
committed by GitHub
27 changed files with 1974 additions and 657 deletions
@@ -1,8 +1,12 @@
import 'package:emojis/emojis.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
import '../stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'main.dart';
import 'routes/routes.dart';
/// Detail screen for a 1:1 chat correspondence /// Detail screen for a 1:1 chat correspondence
class ChatInfoScreen extends StatefulWidget { class ChatInfoScreen extends StatefulWidget {
@@ -24,12 +28,14 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
body: ListView( body: ListView(
children: [ children: [
_buildUserHeader(), _buildUserHeader(),
SizedBox( Container(
height: 8.0, height: 8.0,
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
), ),
_buildOptionListTiles(), _buildOptionListTiles(),
SizedBox( Container(
height: 8.0, height: 8.0,
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
), ),
if ([ if ([
'admin', 'admin',
@@ -73,8 +79,9 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
SizedBox(height: 7.0), SizedBox(height: 7.0),
_buildConnectedTitleState(), _buildConnectedTitleState(),
SizedBox(height: 15.0), SizedBox(height: 15.0),
_OptionListTile( OptionListTile(
title: '@${widget.user.id}', title: '@${widget.user.id}',
tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
trailing: Padding( trailing: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text( child: Text(
@@ -129,14 +136,19 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
StreamBuilder<bool>( StreamBuilder<bool>(
stream: StreamChannel.of(context).channel.isMutedStream, stream: StreamChannel.of(context).channel.isMutedStream,
builder: (context, snapshot) { builder: (context, snapshot) {
return _OptionListTile( return OptionListTile(
tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
title: 'Mute user', title: 'Mute user',
leading: StreamSvgIcon.mute( titleTextStyle: StreamChatTheme.of(context).textTheme.body,
size: 23.0, leading: Padding(
color: StreamChatTheme.of(context) padding: const EdgeInsets.symmetric(horizontal: 22.0),
.colorTheme child: StreamSvgIcon.mute(
.black size: 24.0,
.withOpacity(0.5), color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
),
), ),
trailing: snapshot.data == null trailing: snapshot.data == null
? CircularProgressIndicator() ? CircularProgressIndicator()
@@ -171,40 +183,118 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
// ), // ),
// onTap: () {}, // onTap: () {},
// ), // ),
_OptionListTile( OptionListTile(
title: 'Photos & Videos', title: 'Photos & Videos',
leading: StreamSvgIcon.pictures( tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
size: 32.0, titleTextStyle: StreamChatTheme.of(context).textTheme.body,
color: leading: Padding(
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: StreamSvgIcon.pictures(
size: 36.0,
color:
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
),
),
trailing: StreamSvgIcon.right(
color: StreamChatTheme.of(context).colorTheme.grey,
), ),
trailing: StreamSvgIcon.right(),
onTap: () { onTap: () {
Navigator.push(context, final channel = StreamChannel.of(context).channel;
MaterialPageRoute(builder: (context) => _MediaDisplayScreen()));
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: MessageSearchBloc(
child: ChannelMediaDisplayScreen(
sortOptions: [
SortOption(
'created_at',
direction: SortOption.ASC,
),
],
paginationParams: PaginationParams(limit: 20),
onShowMessage: (m, c) async {
final client = StreamChat.of(context).client;
final message = m;
final channel = client.channel(
c.type,
id: c.id,
);
if (channel.state == null) {
await channel.watch();
}
Navigator.pushNamed(
context,
Routes.CHANNEL_PAGE,
arguments: ChannelPageArgs(
channel: channel,
initialMessage: message,
),
);
},
),
),
),
),
);
}, },
), ),
_OptionListTile( OptionListTile(
title: 'Files', title: 'Files',
leading: StreamSvgIcon.files( tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
size: 32.0, titleTextStyle: StreamChatTheme.of(context).textTheme.body,
color: leading: Padding(
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: StreamSvgIcon.files(
size: 32.0,
color:
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
),
),
trailing: StreamSvgIcon.right(
color: StreamChatTheme.of(context).colorTheme.grey,
), ),
trailing: StreamSvgIcon.right(),
onTap: () { onTap: () {
Navigator.push(context, final channel = StreamChannel.of(context).channel;
MaterialPageRoute(builder: (context) => _FileDisplayScreen()));
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: MessageSearchBloc(
child: ChannelFileDisplayScreen(
sortOptions: [
SortOption(
'created_at',
direction: SortOption.ASC,
),
],
paginationParams: PaginationParams(limit: 20),
),
),
),
),
);
}, },
), ),
_OptionListTile( OptionListTile(
title: 'Shared groups', title: 'Shared groups',
leading: StreamSvgIcon.Icon_group( tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
size: 24.0, titleTextStyle: StreamChatTheme.of(context).textTheme.body,
color: leading: Padding(
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), padding: const EdgeInsets.symmetric(horizontal: 22.0),
child: StreamSvgIcon.Icon_group(
size: 24.0,
color:
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
),
),
trailing: StreamSvgIcon.right(
color: StreamChatTheme.of(context).colorTheme.grey,
), ),
trailing: StreamSvgIcon.right(),
onTap: () { onTap: () {
Navigator.push( Navigator.push(
context, context,
@@ -218,16 +308,23 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
} }
Widget _buildDeleteListTile() { Widget _buildDeleteListTile() {
return _OptionListTile( return OptionListTile(
title: 'Delete conversation', title: 'Delete Conversation',
leading: StreamSvgIcon.delete( tileColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
color: Colors.red, titleTextStyle: StreamChatTheme.of(context).textTheme.body.copyWith(
size: 24.0, color: StreamChatTheme.of(context).colorTheme.accentRed,
),
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 22.0),
child: StreamSvgIcon.delete(
color: StreamChatTheme.of(context).colorTheme.accentRed,
size: 24.0,
),
), ),
onTap: () { onTap: () {
_showDeleteDialog(); _showDeleteDialog();
}, },
titleColor: Colors.red, titleColor: StreamChatTheme.of(context).colorTheme.accentRed,
); );
} }
@@ -239,7 +336,7 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
question: 'Are you sure you want to delete this conversation?', question: 'Are you sure you want to delete this conversation?',
cancelText: 'CANCEL', cancelText: 'CANCEL',
icon: StreamSvgIcon.delete( icon: StreamSvgIcon.delete(
color: Colors.red, color: StreamChatTheme.of(context).colorTheme.accentRed,
), ),
); );
var channel = StreamChannel.of(context).channel; var channel = StreamChannel.of(context).channel;
@@ -302,72 +399,6 @@ class _ChatInfoScreenState extends State<ChatInfoScreen> {
} }
} }
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 { class _SharedGroupsScreen extends StatefulWidget {
final User mainUser; final User mainUser;
final User otherUser; final User otherUser;
@@ -430,12 +461,46 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
}, },
), ),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.data == null) { if (!snapshot.hasData) {
return Center( return Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
); );
} }
if (snapshot.data.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
StreamSvgIcon.message(
size: 136.0,
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
),
SizedBox(height: 16.0),
Text(
'No Shared Groups',
style: TextStyle(
fontSize: 14.0,
color: StreamChatTheme.of(context).colorTheme.black,
),
),
SizedBox(height: 8.0),
Text(
'Group shared with User will appear here.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.0,
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
),
),
],
),
);
}
return ListView.builder( return ListView.builder(
itemCount: snapshot.data.length, itemCount: snapshot.data.length,
itemBuilder: (context, position) { itemBuilder: (context, position) {
@@ -530,75 +595,3 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> {
); );
} }
} }
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,
),
);
}
}
File diff suppressed because it is too large Load Diff
+85
View File
@@ -1,7 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:example/chat_info_screen.dart';
import 'package:example/choose_user_page.dart'; import 'package:example/choose_user_page.dart';
import 'package:example/group_info_screen.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -394,6 +396,32 @@ class _ChannelListPageState extends State<ChannelListPage> {
limit: 20, limit: 20,
), ),
channelWidget: ChannelPage(), channelWidget: ChannelPage(),
onViewInfoTap: (channel) {
if (channel.memberCount == 2 &&
channel.isDistinct) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: ChatInfoScreen(
user: channel.state.members.first.user,
),
),
),
);
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: GroupInfoScreen(),
),
),
);
}
},
), ),
), ),
), ),
@@ -434,6 +462,44 @@ class ChannelPage extends StatelessWidget {
backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow, backgroundColor: StreamChatTheme.of(context).colorTheme.whiteSnow,
appBar: ChannelHeader( appBar: ChannelHeader(
showTypingIndicator: false, showTypingIndicator: false,
onImageTap: () async {
var channel = StreamChannel.of(context).channel;
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(
child: ChatInfoScreen(
user: otherUser.user,
),
channel: channel,
),
),
);
if (pop == true) {
Navigator.pop(context);
}
}
} else {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
child: GroupInfoScreen(),
channel: channel,
),
),
);
}
},
), ),
body: Column( body: Column(
children: <Widget>[ children: <Widget>[
@@ -449,6 +515,25 @@ class ChannelPage extends StatelessWidget {
parent: parentMessage, parent: parentMessage,
); );
}, },
onShowMessage: (m, c) async {
final client = StreamChat.of(context).client;
final message = m;
final channel = client.channel(
c.type,
id: c.id,
);
if (channel.state == null) {
await channel.watch();
}
Navigator.pushReplacementNamed(
context,
Routes.CHANNEL_PAGE,
arguments: ChannelPageArgs(
channel: channel,
initialMessage: message,
),
);
},
), ),
Positioned( Positioned(
bottom: 0, bottom: 0,
+16
View File
@@ -7,6 +7,8 @@ import '../main.dart';
import '../group_chat_details_screen.dart'; import '../group_chat_details_screen.dart';
import '../new_group_chat_screen.dart'; import '../new_group_chat_screen.dart';
import '../new_chat_screen.dart'; import '../new_chat_screen.dart';
import '../chat_info_screen.dart';
import '../group_info_screen.dart';
class AppRoutes { class AppRoutes {
/// Add entry for new route here /// Add entry for new route here
@@ -63,6 +65,20 @@ class AppRoutes {
selectedUsers: args, selectedUsers: args,
); );
}); });
case Routes.CHAT_INFO_SCREEN:
return MaterialPageRoute(
settings: const RouteSettings(name: Routes.CHAT_INFO_SCREEN),
builder: (_) {
return ChatInfoScreen(
user: args,
);
});
case Routes.GROUP_INFO_SCREEN:
return MaterialPageRoute(
settings: const RouteSettings(name: Routes.GROUP_INFO_SCREEN),
builder: (_) {
return GroupInfoScreen();
});
} }
} }
} }
+2
View File
@@ -7,4 +7,6 @@ class Routes {
static const String NEW_CHAT = '/new_chat'; static const String NEW_CHAT = '/new_chat';
static const String NEW_GROUP_CHAT = '/new_group_chat'; static const String NEW_GROUP_CHAT = '/new_group_chat';
static const String NEW_GROUP_CHAT_DETAILS = '/new_group_chat_details'; static const String NEW_GROUP_CHAT_DETAILS = '/new_group_chat_details';
static const String CHAT_INFO_SCREEN = '/chat_info_screen';
static const String GROUP_INFO_SCREEN = '/group_info_screen';
} }
+1 -1
View File
@@ -1,6 +1,6 @@
name: example name: example
description: A new Flutter project. description: A new Flutter project.
version: 1.0.106+109 version: 1.0.107+110
environment: environment:
sdk: ">=2.2.2 <3.0.0" sdk: ">=2.2.2 <3.0.0"
+20 -8
View File
@@ -129,16 +129,22 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
height: 24.0, height: 24.0,
), ),
OptionListTile( OptionListTile(
leading: StreamSvgIcon.user( leading: Padding(
color: StreamChatTheme.of(context).colorTheme.grey, padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: StreamSvgIcon.user(
color: StreamChatTheme.of(context).colorTheme.grey,
),
), ),
title: 'View Info', title: 'View Info',
onTap: widget.onViewInfoTap, onTap: widget.onViewInfoTap,
), ),
if (!channel.isDistinct) if (!channel.isDistinct)
OptionListTile( OptionListTile(
leading: StreamSvgIcon.userRemove( leading: Padding(
color: StreamChatTheme.of(context).colorTheme.grey, padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: StreamSvgIcon.userRemove(
color: StreamChatTheme.of(context).colorTheme.grey,
),
), ),
title: 'Leave Group', title: 'Leave Group',
onTap: () async { onTap: () async {
@@ -147,8 +153,11 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
), ),
if (isOwner) if (isOwner)
OptionListTile( OptionListTile(
leading: StreamSvgIcon.delete( leading: Padding(
color: StreamChatTheme.of(context).colorTheme.accentRed, padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: StreamSvgIcon.delete(
color: StreamChatTheme.of(context).colorTheme.accentRed,
),
), ),
title: 'Delete Conversation', title: 'Delete Conversation',
titleColor: StreamChatTheme.of(context).colorTheme.accentRed, titleColor: StreamChatTheme.of(context).colorTheme.accentRed,
@@ -157,8 +166,11 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
}, },
), ),
OptionListTile( OptionListTile(
leading: StreamSvgIcon.close_small( leading: Padding(
color: StreamChatTheme.of(context).colorTheme.grey, padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: StreamSvgIcon.close_small(
color: StreamChatTheme.of(context).colorTheme.grey,
),
), ),
title: 'Cancel', title: 'Cancel',
onTap: () { 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 '../stream_chat_flutter.dart';
import './channel_name.dart'; import './channel_name.dart';
import 'channel_image.dart'; import 'channel_image.dart';
import 'chat_info_screen.dart';
import 'stream_channel.dart'; import 'stream_channel.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header.png) /// ![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), padding: const EdgeInsets.only(right: 10.0),
child: Center( child: Center(
child: ChannelImage( child: ChannelImage(
onTap: onImageTap ?? 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);
}
}
}
},
), ),
), ),
), ),
+14 -20
View File
@@ -22,6 +22,8 @@ typedef ChannelTapCallback = void Function(Channel, Widget);
/// Builder used to create a custom [ChannelPreview] from a [Channel] /// Builder used to create a custom [ChannelPreview] from a [Channel]
typedef ChannelPreviewBuilder = Widget Function(BuildContext, 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.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_list_view_paint.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.swipeToAction = false,
this.pullToRefresh = true, this.pullToRefresh = true,
this.crossAxisCount = 1, this.crossAxisCount = 1,
this.padding,
this.selectedChannels = const [], this.selectedChannels = const [],
this.onViewInfoTap,
}) : super(key: key); }) : super(key: key);
/// The builder that will be used in case of error /// 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. /// The number of children in the cross axis.
final int crossAxisCount; final int crossAxisCount;
/// The amount of space by which to inset the children.
final EdgeInsetsGeometry padding;
final List<Channel> selectedChannels; final List<Channel> selectedChannels;
final ViewInfoCallback onViewInfoTap;
@override @override
_ChannelListViewState createState() => _ChannelListViewState(); _ChannelListViewState createState() => _ChannelListViewState();
} }
@@ -277,6 +286,7 @@ class _ChannelListViewState extends State<ChannelListView>
if (channels.isNotEmpty) { if (channels.isNotEmpty) {
if (widget.crossAxisCount > 1) { if (widget.crossAxisCount > 1) {
child = GridView.builder( child = GridView.builder(
padding: widget.padding,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: widget.crossAxisCount), crossAxisCount: widget.crossAxisCount),
itemCount: channels.length, itemCount: channels.length,
@@ -288,6 +298,7 @@ class _ChannelListViewState extends State<ChannelListView>
); );
} else { } else {
child = ListView.separated( child = ListView.separated(
padding: widget.padding,
physics: AlwaysScrollableScrollPhysics(), physics: AlwaysScrollableScrollPhysics(),
itemCount: itemCount:
channels.isNotEmpty ? channels.length + 1 : channels.length, channels.isNotEmpty ? channels.length + 1 : channels.length,
@@ -315,6 +326,7 @@ class _ChannelListViewState extends State<ChannelListView>
Widget _buildLoadingWidget() { Widget _buildLoadingWidget() {
return ListView( return ListView(
padding: widget.padding,
physics: AlwaysScrollableScrollPhysics(), physics: AlwaysScrollableScrollPhysics(),
children: List.generate( children: List.generate(
25, 25,
@@ -340,9 +352,7 @@ class _ChannelListViewState extends State<ChannelListView>
highlightColor: StreamChatTheme.of(context).colorTheme.whiteSmoke, highlightColor: StreamChatTheme.of(context).colorTheme.whiteSmoke,
child: Column( child: Column(
children: [ children: [
SizedBox( SizedBox(height: 4.0),
height: 4.0,
),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
@@ -574,23 +584,7 @@ class _ChannelListViewState extends State<ChannelListView>
return StreamChannel( return StreamChannel(
child: ChannelBottomSheet( child: ChannelBottomSheet(
onViewInfoTap: () { onViewInfoTap: () {
if (channel.memberCount == 2 && widget.onViewInfoTap(channel);
channel.isDistinct) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: ChatInfoScreen(
user:
channel.state.members.first.user,
),
),
),
);
}
// TODO: Add group screen
}, },
), ),
channel: 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 '../stream_chat_flutter.dart';
import 'channel_name.dart'; import 'channel_name.dart';
import 'channel_unread_indicator.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.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview_paint.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( leading: ChannelImage(
onTap: onImageTap ?? 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,
),
),
),
);
}
}
},
), ),
title: Row( title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
+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_footer.dart';
import 'package:stream_chat_flutter/src/image_header.dart'; import 'package:stream_chat_flutter/src/image_header.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
import 'stream_channel.dart';
import '../stream_chat_flutter.dart'; import '../stream_chat_flutter.dart';
typedef ShowMessageCallback = void Function(Message message, Channel channel);
/// A full screen image widget /// A full screen image widget
class FullScreenMedia extends StatefulWidget { class FullScreenMedia extends StatefulWidget {
/// The url of the image /// The url of the image
@@ -18,6 +21,7 @@ class FullScreenMedia extends StatefulWidget {
final int startIndex; final int startIndex;
final String userName; final String userName;
final DateTime sentAt; final DateTime sentAt;
final ShowMessageCallback onShowMessage;
/// Instantiate a new FullScreenImage /// Instantiate a new FullScreenImage
const FullScreenMedia({ const FullScreenMedia({
@@ -27,6 +31,7 @@ class FullScreenMedia extends StatefulWidget {
this.startIndex = 0, this.startIndex = 0,
this.userName = '', this.userName = '',
this.sentAt, this.sentAt,
this.onShowMessage,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -167,6 +172,10 @@ class _FullScreenMediaState extends State<FullScreenMedia>
message: widget.message, message: widget.message,
urls: widget.mediaAttachments, urls: widget.mediaAttachments,
currentIndex: _currentPage, currentIndex: _currentPage,
onShowMessage: () {
widget.onShowMessage(
widget.message, StreamChannel.of(context).channel);
},
), ),
ImageFooter( ImageFooter(
currentPage: _currentPage, currentPage: _currentPage,
+4
View File
@@ -12,6 +12,7 @@ class GiphyAttachment extends StatelessWidget {
final MessageTheme messageTheme; final MessageTheme messageTheme;
final Message message; final Message message;
final Size size; final Size size;
final ShowMessageCallback onShowMessage;
const GiphyAttachment({ const GiphyAttachment({
Key key, Key key,
@@ -19,6 +20,7 @@ class GiphyAttachment extends StatelessWidget {
this.messageTheme, this.messageTheme,
this.message, this.message,
this.size, this.size,
this.onShowMessage,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -75,6 +77,7 @@ class GiphyAttachment extends StatelessWidget {
userName: message.user.name, userName: message.user.name,
sentAt: message.createdAt, sentAt: message.createdAt,
message: message, message: message,
onShowMessage: onShowMessage,
), ),
); );
})); }));
@@ -327,6 +330,7 @@ class GiphyAttachment extends StatelessWidget {
userName: message.user.name, userName: message.user.name,
sentAt: message.createdAt, sentAt: message.createdAt,
message: message, message: message,
onShowMessage: onShowMessage,
), ),
); );
})); }));
+9 -5
View File
@@ -15,9 +15,15 @@ class ImageActionsModal extends StatelessWidget {
final String sentAt; final String sentAt;
final List<Attachment> urls; final List<Attachment> urls;
final currentIndex; final currentIndex;
final VoidCallback onShowMessage;
ImageActionsModal( ImageActionsModal(
{this.message, this.userName, this.sentAt, this.urls, this.currentIndex}); {this.message,
this.userName,
this.sentAt,
this.urls,
this.currentIndex,
this.onShowMessage});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -134,10 +140,8 @@ class ImageActionsModal extends StatelessWidget {
size: 24.0, size: 24.0,
color: color:
StreamChatTheme.of(context).colorTheme.black, StreamChatTheme.of(context).colorTheme.black,
), () { ),
Navigator.pop(context); onShowMessage),
Navigator.pop(context);
}),
_buildButton( _buildButton(
context, context,
'Save ${urls[currentIndex].type == 'video' ? 'Video' : 'Image'}', 'Save ${urls[currentIndex].type == 'video' ? 'Video' : 'Image'}',
+8 -2
View File
@@ -12,6 +12,8 @@ class ImageAttachment extends StatelessWidget {
final Message message; final Message message;
final MessageTheme messageTheme; final MessageTheme messageTheme;
final Size size; final Size size;
final bool showTitle;
final ShowMessageCallback onShowMessage;
const ImageAttachment({ const ImageAttachment({
Key key, Key key,
@@ -19,6 +21,8 @@ class ImageAttachment extends StatelessWidget {
@required this.message, @required this.message,
@required this.size, @required this.size,
this.messageTheme, this.messageTheme,
this.showTitle = true,
this.onShowMessage,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -54,6 +58,7 @@ class ImageAttachment extends StatelessWidget {
userName: message.user.name, userName: message.user.name,
sentAt: message.createdAt, sentAt: message.createdAt,
message: message, message: message,
onShowMessage: onShowMessage,
), ),
); );
}, },
@@ -83,7 +88,7 @@ class ImageAttachment extends StatelessWidget {
), ),
), ),
), ),
if (attachment.title != null) if (showTitle && attachment.title != null)
Material( Material(
color: messageTheme.messageBackgroundColor, color: messageTheme.messageBackgroundColor,
child: AttachmentTitle( 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( Positioned.fill(
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
+115 -374
View File
@@ -12,11 +12,10 @@ import 'package:flutter/material.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:stream_chat/stream_chat.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/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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. /// Callback to call when pressing the back button.
/// By default it calls [Navigator.pop] /// By default it calls [Navigator.pop]
final VoidCallback onBackPressed; final VoidCallback onBackPressed;
@@ -48,21 +47,25 @@ class ImageFooter extends StatefulWidget {
this.message, this.message,
this.videoPackages, this.videoPackages,
this.mediaSelectedCallBack, this.mediaSelectedCallBack,
}) : super(key: key); }) : preferredSize = Size.fromHeight(kToolbarHeight),
super(key: key);
@override @override
_ImageFooterState createState() => _ImageFooterState(); _ImageFooterState createState() => _ImageFooterState();
@override
final Size preferredSize;
} }
class _ImageFooterState extends State<ImageFooter> { class _ImageFooterState extends State<ImageFooter> {
bool _userSearchMode = false; bool _userSearchMode = false;
TextEditingController _searchController; TextEditingController _searchController;
TextEditingController _messageController = TextEditingController(); final TextEditingController _messageController = TextEditingController();
FocusNode _messageFocusNode = FocusNode(); final FocusNode _messageFocusNode = FocusNode();
String _channelNameQuery; String _channelNameQuery;
List<Channel> _selectedChannels = []; final List<Channel> _selectedChannels = [];
bool _loading = false; bool _loading = false;
Timer _debounce; Timer _debounce;
@@ -99,54 +102,73 @@ class _ImageFooterState extends State<ImageFooter> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SafeArea( return SizedBox.fromSize(
child: Container( size: Size(
color: MediaQuery.of(context).size.width,
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color, MediaQuery.of(context).padding.bottom + widget.preferredSize.height,
padding: EdgeInsets.symmetric(vertical: 8.0), ),
child: Row( child: MediaQuery.removePadding(
mainAxisAlignment: MainAxisAlignment.spaceBetween, context: context,
children: [ removeTop: true,
IconButton( child: BottomAppBar(
icon: StreamSvgIcon.icon_SHARE( color: StreamChatTheme.of(context).colorTheme.white,
size: 24.0, child: Row(
color: StreamChatTheme.of(context).colorTheme.black, 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: () { InkWell(
_buildShareModal(context); onTap: widget.onTitleTap,
}, child: Container(
), child: Column(
InkWell( crossAxisAlignment: CrossAxisAlignment.center,
onTap: widget.onTitleTap, mainAxisAlignment: MainAxisAlignment.center,
child: Container( mainAxisSize: MainAxisSize.min,
child: Column( children: <Widget>[
crossAxisAlignment: CrossAxisAlignment.center, Text(
mainAxisAlignment: MainAxisAlignment.center, '${widget.currentPage + 1} of ${widget.totalPages}',
mainAxisSize: MainAxisSize.min, style:
children: <Widget>[ StreamChatTheme.of(context).textTheme.headlineBold,
Text( ),
'${widget.currentPage + 1} of ${widget.totalPages}', ],
style: StreamChatTheme.of(context).textTheme.headlineBold, ),
),
],
), ),
), ),
), IconButton(
IconButton( icon: StreamSvgIcon.Icon_grid(
icon: StreamSvgIcon.Icon_grid( color: StreamChatTheme.of(context).colorTheme.black,
color: StreamChatTheme.of(context).colorTheme.black, ),
onPressed: () {
_buildPhotosModal(context);
},
), ),
onPressed: () { ],
_buildPhotosModal(context); ),
},
),
],
), ),
), ),
); );
} }
void _buildPhotosModal(context) { Widget _buildPhotosModal(context) {
var videoAttachments = widget.mediaAttachments var videoAttachments = widget.mediaAttachments
.where((element) => element.type == 'video') .where((element) => element.type == 'video')
.toList(); .toList();
@@ -247,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) { Widget _buildShareTextInputSection(modalSetState) {
return Align( return Align(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: Container( child: BottomAppBar(
color: StreamChatTheme.of(context).colorTheme.white, child: Container(
height: 56.0, height: 40.0,
child: _loading margin: const EdgeInsets.symmetric(
? Center( vertical: 8,
child: Padding( horizontal: 8,
padding: const EdgeInsets.all(8.0), ),
child: CircularProgressIndicator(), child: _loading
), ? Center(
) child: Padding(
: Row( padding: const EdgeInsets.all(8.0),
children: [ child: CircularProgressIndicator(),
Expanded( ),
child: Padding( )
padding: const EdgeInsets.only(left: 8.0), : Row(
children: [
Expanded(
child: TextField( child: TextField(
controller: _messageController, controller: _messageController,
focusNode: _messageFocusNode, focusNode: _messageFocusNode,
@@ -557,11 +300,10 @@ class _ImageFooterState extends State<ImageFooter> {
setState(() {}); setState(() {});
}, },
decoration: InputDecoration( decoration: InputDecoration(
isDense: true,
prefixText: ' ', prefixText: ' ',
hintText: 'Add a comment', hintText: 'Add a comment',
border: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(32.0), borderRadius: BorderRadius.circular(24.0),
borderSide: BorderSide( borderSide: BorderSide(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
@@ -570,49 +312,48 @@ class _ImageFooterState extends State<ImageFooter> {
), ),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(32.0), borderRadius: BorderRadius.circular(24.0),
borderSide: BorderSide( borderSide: BorderSide(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.black .black
.withOpacity(0.16), .withOpacity(0.16),
)), )),
contentPadding: EdgeInsets.symmetric(vertical: 12.0), contentPadding: const EdgeInsets.all(0),
), ),
), ),
), ),
), SizedBox(width: 8),
IconTheme( IconTheme(
data: StreamChatTheme.of(context) data: StreamChatTheme.of(context)
.channelTheme .channelTheme
.messageInputButtonIconTheme, .messageInputButtonIconTheme,
child: Center( child: IconButton(
child: Padding( onPressed: () async {
padding: const EdgeInsets.all(8.0), modalSetState(() => _loading = true);
child: InkWell( await sendMessage();
onTap: () async { modalSetState(() => _loading = false);
modalSetState(() { },
_loading = true; splashRadius: 24,
}); visualDensity: VisualDensity.compact,
await sendMessage(); constraints: BoxConstraints.tightFor(
modalSetState(() { height: 24,
_loading = false; width: 24,
}); ),
}, padding: EdgeInsets.zero,
child: Transform.rotate( icon: Transform.rotate(
angle: -pi / 2, angle: -pi / 2,
child: StreamSvgIcon.Icon_send_message( child: StreamSvgIcon.Icon_send_message(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.accentBlue, .accentBlue,
),
), ),
), ),
), ),
), ),
), ],
], ),
), ),
), ),
); );
} }
+3
View File
@@ -10,11 +10,13 @@ class ImageGroup extends StatelessWidget {
@required this.images, @required this.images,
@required this.message, @required this.message,
@required this.size, @required this.size,
this.onShowMessage,
}) : super(key: key); }) : super(key: key);
final List<Attachment> images; final List<Attachment> images;
final Message message; final Message message;
final Size size; final Size size;
final ShowMessageCallback onShowMessage;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -119,6 +121,7 @@ class ImageGroup extends StatelessWidget {
userName: message.user.name, userName: message.user.name,
sentAt: message.createdAt, sentAt: message.createdAt,
message: message, message: message,
onShowMessage: onShowMessage,
), ),
), ),
), ),
+5
View File
@@ -13,6 +13,9 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
/// By default it calls [Navigator.pop] /// By default it calls [Navigator.pop]
final VoidCallback onBackPressed; final VoidCallback onBackPressed;
/// Callback to call when pressing the show message button.
final VoidCallback onShowMessage;
/// Callback to call when the header is tapped. /// Callback to call when the header is tapped.
final VoidCallback onTitleTap; final VoidCallback onTitleTap;
@@ -35,6 +38,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
this.currentIndex, this.currentIndex,
this.showBackButton = true, this.showBackButton = true,
this.onBackPressed, this.onBackPressed,
this.onShowMessage,
this.onTitleTap, this.onTitleTap,
this.onImageTap, this.onImageTap,
this.userName = '', this.userName = '',
@@ -111,6 +115,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
message: message, message: message,
urls: urls, urls: urls,
currentIndex: currentIndex, currentIndex: currentIndex,
onShowMessage: onShowMessage,
), ),
); );
}); });
+5
View File
@@ -116,6 +116,7 @@ class MessageListView extends StatefulWidget {
this.scrollController, this.scrollController,
this.itemPositionListener, this.itemPositionListener,
this.highlightInitialMessage = false, this.highlightInitialMessage = false,
this.onShowMessage,
}) : super(key: key); }) : super(key: key);
/// Function used to build a custom message widget /// Function used to build a custom message widget
@@ -162,6 +163,8 @@ class MessageListView extends StatefulWidget {
/// Also See [StreamChannel] /// Also See [StreamChannel]
final bool highlightInitialMessage; final bool highlightInitialMessage;
final ShowMessageCallback onShowMessage;
@override @override
_MessageListViewState createState() => _MessageListViewState(); _MessageListViewState createState() => _MessageListViewState();
} }
@@ -711,6 +714,7 @@ class _MessageListViewState extends State<MessageListView> {
messageTheme: isMyMessage messageTheme: isMyMessage
? StreamChatTheme.of(context).ownMessageTheme ? StreamChatTheme.of(context).ownMessageTheme
: StreamChatTheme.of(context).otherMessageTheme, : StreamChatTheme.of(context).otherMessageTheme,
onShowMessage: widget.onShowMessage,
); );
} }
@@ -788,6 +792,7 @@ class _MessageListViewState extends State<MessageListView> {
: StreamChatTheme.of(context).otherMessageTheme, : StreamChatTheme.of(context).otherMessageTheme,
readList: readList, readList: readList,
allRead: allRead, allRead: allRead,
onShowMessage: widget.onShowMessage,
); );
if (!initialMessageHighlightComplete && if (!initialMessageHighlightComplete &&
+2
View File
@@ -55,6 +55,7 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
/// Calls [Client.search] updating [queryMessagesLoading] stream /// Calls [Client.search] updating [queryMessagesLoading] stream
Future<void> search({ Future<void> search({
Map<String, dynamic> filter, Map<String, dynamic> filter,
Map<String, dynamic> messageFilter,
List<SortOption> sort, List<SortOption> sort,
String query, String query,
PaginationParams pagination, PaginationParams pagination,
@@ -78,6 +79,7 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
sort, sort,
query, query,
pagination, pagination,
messageFilters: messageFilter,
); );
if (clear) { if (clear) {
+7
View File
@@ -119,6 +119,8 @@ class MessageWidget extends StatefulWidget {
final List<Read> readList; final List<Read> readList;
final ShowMessageCallback onShowMessage;
/// If true show the users username next to the timestamp of the message /// If true show the users username next to the timestamp of the message
final bool showUsername; final bool showUsername;
final bool showTimestamp; final bool showTimestamp;
@@ -157,6 +159,7 @@ class MessageWidget extends StatefulWidget {
this.onUserAvatarTap, this.onUserAvatarTap,
this.onLinkTap, this.onLinkTap,
this.onMessageActions, this.onMessageActions,
this.onShowMessage,
this.editMessageInputBuilder, this.editMessageInputBuilder,
this.textBuilder, this.textBuilder,
Map<String, AttachmentBuilder> customAttachmentBuilders, Map<String, AttachmentBuilder> customAttachmentBuilders,
@@ -179,6 +182,7 @@ class MessageWidget extends StatefulWidget {
MediaQuery.of(context).size.width * 0.8, MediaQuery.of(context).size.width * 0.8,
MediaQuery.of(context).size.height * 0.3, MediaQuery.of(context).size.height * 0.3,
), ),
onShowMessage: onShowMessage,
); );
}, },
'video': (context, message, attachment) { 'video': (context, message, attachment) {
@@ -190,6 +194,7 @@ class MessageWidget extends StatefulWidget {
MediaQuery.of(context).size.height * 0.3, MediaQuery.of(context).size.height * 0.3,
), ),
message: message, message: message,
onShowMessage: onShowMessage,
); );
}, },
'giphy': (context, message, attachment) { 'giphy': (context, message, attachment) {
@@ -201,6 +206,7 @@ class MessageWidget extends StatefulWidget {
MediaQuery.of(context).size.width * 0.8, MediaQuery.of(context).size.width * 0.8,
MediaQuery.of(context).size.height * 0.3, MediaQuery.of(context).size.height * 0.3,
), ),
onShowMessage: onShowMessage,
); );
}, },
'file': (context, message, attachment) { 'file': (context, message, attachment) {
@@ -710,6 +716,7 @@ class _MessageWidgetState extends State<MessageWidget> {
), ),
images: images, images: images,
message: widget.message, 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 { class OptionListTile extends StatelessWidget {
final String title; final String title;
final StreamSvgIcon leading; final Widget leading;
final Widget trailing; final Widget trailing;
final VoidCallback onTap; final VoidCallback onTap;
final Color titleColor; final Color titleColor;
final Color tileColor;
final Color separatorColor;
final TextStyle titleTextStyle;
OptionListTile({ OptionListTile({
this.title, this.title,
@@ -15,6 +18,9 @@ class OptionListTile extends StatelessWidget {
this.trailing, this.trailing,
this.onTap, this.onTap,
this.titleColor, this.titleColor,
this.tileColor,
this.separatorColor,
this.titleTextStyle,
}); });
@override @override
@@ -22,21 +28,19 @@ class OptionListTile extends StatelessWidget {
return Column( return Column(
children: [ children: [
Container( Container(
color: StreamChatTheme.of(context).colorTheme.greyGainsboro, color: separatorColor ??
StreamChatTheme.of(context).colorTheme.greyGainsboro,
height: 1.0, height: 1.0,
), ),
Material( Material(
color: StreamChatTheme.of(context).colorTheme.white, color: tileColor ?? StreamChatTheme.of(context).colorTheme.white,
child: Container( child: Container(
height: 63.0, height: 63.0,
child: InkWell( child: InkWell(
onTap: onTap, onTap: onTap,
child: Row( child: Row(
children: [ children: [
if (leading != null) if (leading != null) Center(child: leading),
Expanded(
child: Center(child: leading),
),
if (leading == null) if (leading == null)
SizedBox( SizedBox(
width: 16.0, width: 16.0,
@@ -45,14 +49,15 @@ class OptionListTile extends StatelessWidget {
flex: 4, flex: 4,
child: Text( child: Text(
title, title,
style: titleColor == null style: titleTextStyle ??
? StreamChatTheme.of(context).textTheme.bodyBold (titleColor == null
: StreamChatTheme.of(context) ? StreamChatTheme.of(context).textTheme.bodyBold
.textTheme : StreamChatTheme.of(context)
.bodyBold .textTheme
.copyWith( .bodyBold
color: titleColor, .copyWith(
), color: titleColor,
)),
), ),
), ),
Expanded( Expanded(
+12
View File
@@ -781,4 +781,16 @@ class StreamSvgIcon extends StatelessWidget {
height: 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,
);
}
} }
+3
View File
@@ -14,6 +14,7 @@ class VideoAttachment extends StatefulWidget {
final MessageTheme messageTheme; final MessageTheme messageTheme;
final Size size; final Size size;
final Message message; final Message message;
final ShowMessageCallback onShowMessage;
VideoAttachment({ VideoAttachment({
Key key, Key key,
@@ -21,6 +22,7 @@ class VideoAttachment extends StatefulWidget {
@required this.messageTheme, @required this.messageTheme,
this.message, this.message,
this.size, this.size,
this.onShowMessage,
}) : super(key: key); }) : super(key: key);
@override @override
@@ -95,6 +97,7 @@ class _VideoAttachmentState extends State<VideoAttachment> {
userName: widget.message.user.name, userName: widget.message.user.name,
sentAt: widget.message.createdAt, sentAt: widget.message.createdAt,
message: widget.message, message: widget.message,
onShowMessage: widget.onShowMessage,
), ),
), ),
), ),
+3 -1
View File
@@ -43,4 +43,6 @@ export 'src/message_search_bloc.dart';
export 'src/message_search_item.dart'; export 'src/message_search_item.dart';
export 'src/message_search_list_view.dart'; export 'src/message_search_list_view.dart';
export 'src/unread_indicator.dart'; export 'src/unread_indicator.dart';
export 'src/chat_info_screen.dart'; export 'src/option_list_tile.dart';
export 'src/channel_file_display_screen.dart';
export 'src/channel_media_display_screen.dart';
+5
View File
@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 11C14.2091 11 16 9.20914 16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7C8 9.20914 9.79086 11 12 11ZM12 9C13.1046 9 14 8.10457 14 7C14 5.89543 13.1046 5 12 5C10.8954 5 10 5.89543 10 7C10 8.10457 10.8954 9 12 9Z" fill="#7A7A7A"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 12C6.4686 12 4 15.6324 4 18C4 18.5523 3.55228 19 3 19C2.44771 19 2 18.5523 2 18C2 14.3676 5.5314 10 12 10C13.9955 10 15.7139 10.4121 17.1402 11.0991C17.6378 11.3387 17.8469 11.9363 17.6072 12.4339C17.3676 12.9315 16.7699 13.1406 16.2724 12.9009C15.1351 12.3532 13.716 12 12 12Z" fill="#7A7A7A"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.5 14C18.2239 14 18 14.2239 18 14.5V15.2679L17.3349 14.884C17.0958 14.7459 16.79 14.8278 16.6519 15.067L16.1519 15.933C16.0139 16.1722 16.0958 16.478 16.3349 16.616L17 17L16.3349 17.384C16.0958 17.522 16.0138 17.8278 16.1519 18.067L16.6519 18.933C16.79 19.1722 17.0958 19.2541 17.3349 19.116L18 18.732V19.5C18 19.7761 18.2239 20 18.5 20H19.5C19.7761 20 20 19.7761 20 19.5V18.732L20.6651 19.116C20.9042 19.2541 21.21 19.1722 21.3481 18.933L21.8481 18.067C21.9861 17.8278 21.9042 17.522 21.6651 17.384L21 17L21.665 16.616C21.9042 16.478 21.9861 16.1722 21.8481 15.933L21.3481 15.067C21.21 14.8278 20.9042 14.7459 20.665 14.884L20 15.2679V14.5C20 14.2239 19.7761 14 19.5 14H18.5ZM19 18C19.5523 18 20 17.5523 20 17C20 16.4477 19.5523 16 19 16C18.4477 16 18 16.4477 18 17C18 17.5523 18.4477 18 19 18Z" fill="#7A7A7A"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB