style: provide folder structure

This commit is contained in:
Gordon Hayes
2022-10-06 12:42:57 +02:00
parent 091e8624ca
commit 50018887e1
27 changed files with 244 additions and 238 deletions
@@ -0,0 +1,351 @@
import 'package:example/app.dart';
import 'package:example/pages/home_page.dart';
import 'package:example/utils/localizations.dart';
import 'package:example/routes/routes.dart';
import 'package:example/widgets/stream_version.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'choose_user_page.dart';
class AdvancedOptionsPage extends StatefulWidget {
@override
_AdvancedOptionsPageState createState() => _AdvancedOptionsPageState();
}
class _AdvancedOptionsPageState extends State<AdvancedOptionsPage> {
final _formKey = GlobalKey<FormState>();
final TextEditingController _apiKeyController = TextEditingController();
String? _apiKeyError;
final TextEditingController _userIdController = TextEditingController();
String? _userIdError;
final TextEditingController _userTokenController = TextEditingController();
String? _userTokenError;
final TextEditingController _usernameController = TextEditingController();
bool loading = false;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
appBar: AppBar(
backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
elevation: 1,
centerTitle: true,
title: Text(
AppLocalizations.of(context).advancedOptions,
style: StreamChatTheme.of(context).textTheme.headlineBold.copyWith(
color: StreamChatTheme.of(context).colorTheme.textHighEmphasis),
),
leading: IconButton(
icon: StreamSvgIcon.left(
color: StreamChatTheme.of(context).colorTheme.textHighEmphasis,
),
onPressed: () {
Navigator.pop(context);
},
),
),
body: Builder(
builder: (context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: _apiKeyController,
onChanged: (_) {
if (_apiKeyError != null) {
setState(() {
_apiKeyError = null;
});
}
},
validator: (value) {
if (value!.isEmpty) {
setState(() {
_apiKeyError = AppLocalizations.of(context)
.apiKeyError
.toUpperCase();
});
return _apiKeyError;
}
return null;
},
style: TextStyle(
fontSize: 14,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis,
),
decoration: InputDecoration(
errorStyle: TextStyle(height: 0, fontSize: 0),
labelStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: _apiKeyError != null
? StreamChatTheme.of(context).colorTheme.accentError
: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
),
border: UnderlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
fillColor: StreamChatTheme.of(context).colorTheme.inputBg,
filled: true,
labelText: _apiKeyError != null
? '${AppLocalizations.of(context).chatApiKey.toUpperCase()}: $_apiKeyError'
: AppLocalizations.of(context).chatApiKey,
),
textInputAction: TextInputAction.next,
),
SizedBox(height: 8),
TextFormField(
controller: _userIdController,
onChanged: (_) {
if (_userIdError != null) {
setState(() {
_userIdError = null;
});
}
},
validator: (value) {
if (value!.isEmpty) {
setState(() {
_userIdError = AppLocalizations.of(context)
.userIdError
.toUpperCase();
});
return _userIdError;
}
return null;
},
style: TextStyle(
fontSize: 14,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis,
),
textInputAction: TextInputAction.next,
decoration: InputDecoration(
errorStyle: TextStyle(height: 0, fontSize: 0),
labelStyle: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: _userIdError != null
? StreamChatTheme.of(context).colorTheme.accentError
: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
),
border: UnderlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
fillColor: StreamChatTheme.of(context).colorTheme.inputBg,
filled: true,
labelText: _userIdError != null
? '${AppLocalizations.of(context).userId.toUpperCase()}: $_userIdError'
: AppLocalizations.of(context).userId,
),
),
SizedBox(height: 8),
TextFormField(
onChanged: (_) {
if (_userTokenError != null) {
setState(() {
_userTokenError = null;
});
}
},
controller: _userTokenController,
validator: (value) {
if (value!.isEmpty) {
setState(() {
_userTokenError = AppLocalizations.of(context)
.userTokenError
.toUpperCase();
});
return _userTokenError;
}
return null;
},
style: TextStyle(
fontSize: 14,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis,
),
textInputAction: TextInputAction.next,
decoration: InputDecoration(
errorStyle: TextStyle(height: 0, fontSize: 0),
labelStyle: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: _userTokenError != null
? StreamChatTheme.of(context).colorTheme.accentError
: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
),
border: UnderlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
fillColor: StreamChatTheme.of(context).colorTheme.inputBg,
filled: true,
labelText: _userTokenError != null
? '${AppLocalizations.of(context).userToken.toUpperCase()}: $_userTokenError'
: AppLocalizations.of(context).userToken,
),
),
SizedBox(height: 8),
TextFormField(
controller: _usernameController,
textInputAction: TextInputAction.done,
decoration: InputDecoration(
labelStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
),
border: UnderlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
fillColor: StreamChatTheme.of(context).colorTheme.inputBg,
filled: true,
labelText: AppLocalizations.of(context).usernameOptional,
),
),
Spacer(),
ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all<Color>(
Theme.of(context).brightness == Brightness.light
? StreamChatTheme.of(context)
.colorTheme
.accentPrimary
: Colors.white),
elevation: MaterialStateProperty.all<double>(0),
padding: MaterialStateProperty.all<EdgeInsets>(
const EdgeInsets.symmetric(vertical: 16)),
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(26),
),
),
),
child: Text(
AppLocalizations.of(context).login,
style: TextStyle(
fontSize: 16,
color: Theme.of(context).brightness != Brightness.light
? StreamChatTheme.of(context)
.colorTheme
.accentPrimary
: Colors.white,
),
),
onPressed: () async {
if (loading) {
return;
}
if (_formKey.currentState!.validate()) {
final apiKey = _apiKeyController.text;
final userId = _userIdController.text;
final userToken = _userTokenController.text;
final username = _usernameController.text;
loading = true;
showDialog(
barrierDismissible: false,
context: context,
barrierColor:
StreamChatTheme.of(context).colorTheme.overlay,
builder: (context) => Center(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: StreamChatTheme.of(context)
.colorTheme
.barsBg,
),
height: 100,
width: 100,
child: Center(
child: CircularProgressIndicator(),
),
),
),
);
final client = buildStreamChatClient(apiKey);
try {
await client.connectUser(
User(id: userId, extraData: {
'name': username,
}),
userToken,
);
final secureStorage = FlutterSecureStorage();
secureStorage.write(
key: kStreamApiKey,
value: apiKey,
);
secureStorage.write(
key: kStreamUserId,
value: userId,
);
secureStorage.write(
key: kStreamToken,
value: userToken,
);
} catch (e) {
var errorText =
AppLocalizations.of(context).errorConnecting;
if (e is Map) {
errorText = e['message'] ?? errorText;
}
Navigator.pop(context);
setState(() {
_apiKeyError = errorText.toUpperCase();
});
loading = false;
return;
}
loading = false;
await Navigator.pushNamedAndRemoveUntil(
context,
Routes.HOME,
ModalRoute.withName(Routes.HOME),
arguments: HomePageArgs(client),
);
}
},
),
StreamVersion(),
],
),
),
);
},
),
);
}
}
@@ -0,0 +1,156 @@
import 'package:example/utils/localizations.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart';
class ChannelFileDisplayScreen extends StatefulWidget {
final StreamMessageThemeData messageTheme;
const ChannelFileDisplayScreen({
Key? key,
required this.messageTheme,
}) : super(key: key);
@override
State<ChannelFileDisplayScreen> createState() =>
_ChannelFileDisplayScreenState();
}
class _ChannelFileDisplayScreenState extends State<ChannelFileDisplayScreen> {
final Map<String?, VideoPlayerController?> controllerCache = {};
late final controller = StreamMessageSearchListController(
client: StreamChat.of(context).client,
filter: Filter.in_(
'cid',
[StreamChannel.of(context).channel.cid!],
),
messageFilter: Filter.in_(
'attachments.type',
['file'],
),
sort: [
SortOption(
'created_at',
direction: SortOption.ASC,
),
],
limit: 20,
);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
appBar: AppBar(
elevation: 1,
centerTitle: true,
title: Text(
AppLocalizations.of(context).files,
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.textHighEmphasis,
fontSize: 16.0,
),
),
leading: StreamBackButton(),
backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
),
body: ValueListenableBuilder(
valueListenable: controller,
builder: (
BuildContext context,
PagedValue<String, GetMessageResponse> value,
Widget? child,
) {
return value.when(
(items, nextPageKey, error) {
if (items.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
StreamSvgIcon.files(
size: 136.0,
color: StreamChatTheme.of(context).colorTheme.disabled,
),
SizedBox(height: 16.0),
Text(
AppLocalizations.of(context).noFiles,
style: TextStyle(
fontSize: 14.0,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis,
),
),
SizedBox(height: 8.0),
Text(
AppLocalizations.of(context).filesAppearHere,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.0,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
),
),
],
),
);
}
final media = <Attachment, Message>{};
for (var item in items) {
item.message.attachments
.where((e) => e.type == 'file')
.forEach((e) {
media[e] = item.message;
});
}
return LazyLoadScrollView(
onEndOfPage: () async {
if (nextPageKey != null) {
controller.loadMore(nextPageKey);
}
},
child: ListView.builder(
itemBuilder: (context, position) {
return Padding(
padding: const EdgeInsets.all(1.0),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: StreamFileAttachment(
message: media.values.toList()[position],
attachment: media.keys.toList()[position],
),
),
);
},
itemCount: media.length,
),
);
},
loading: () => Center(
child: const CircularProgressIndicator(),
),
error: (_) => Offstage(),
);
},
),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
void initState() {
controller.doInitialLoad();
super.initState();
}
}
@@ -0,0 +1,285 @@
import 'dart:async';
import 'package:example/utils/localizations.dart';
import 'package:example/routes/routes.dart';
import 'package:example/pages/user_mentions_page.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app_badger/flutter_app_badger.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:streaming_shared_preferences/streaming_shared_preferences.dart';
import 'package:example/widgets/channel_list.dart';
class ChannelListPage extends StatefulWidget {
const ChannelListPage({
Key? key,
}) : super(key: key);
@override
_ChannelListPageState createState() => _ChannelListPageState();
}
class _ChannelListPageState extends State<ChannelListPage> {
int _currentIndex = 0;
bool _isSelected(int index) => _currentIndex == index;
List<BottomNavigationBarItem> get _navBarItems {
return <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Stack(
clipBehavior: Clip.none,
children: [
StreamSvgIcon.message(
color: _isSelected(0)
? StreamChatTheme.of(context).colorTheme.textHighEmphasis
: Colors.grey,
),
Positioned(
top: -3,
right: -16,
child: StreamUnreadIndicator(),
),
],
),
label: AppLocalizations.of(context).chats,
),
BottomNavigationBarItem(
icon: Stack(
clipBehavior: Clip.none,
children: [
StreamSvgIcon.mentions(
color: _isSelected(1)
? StreamChatTheme.of(context).colorTheme.textHighEmphasis
: Colors.grey,
),
],
),
label: AppLocalizations.of(context).mentions,
),
];
}
@override
Widget build(BuildContext context) {
final user = StreamChat.of(context).currentUser;
if (user == null) {
return Offstage();
}
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
appBar: StreamChannelListHeader(
onNewChatButtonTap: () {
Navigator.pushNamed(context, Routes.NEW_CHAT);
},
preNavigationCallback: () {
FocusScope.of(context).requestFocus(FocusNode());
},
),
drawer: LeftDrawer(
user: user,
),
drawerEdgeDragWidth: 50,
bottomNavigationBar: BottomNavigationBar(
backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
currentIndex: _currentIndex,
items: _navBarItems,
selectedLabelStyle: StreamChatTheme.of(context).textTheme.footnoteBold,
unselectedLabelStyle:
StreamChatTheme.of(context).textTheme.footnoteBold,
type: BottomNavigationBarType.fixed,
selectedItemColor:
StreamChatTheme.of(context).colorTheme.textHighEmphasis,
unselectedItemColor: Colors.grey,
onTap: (index) {
setState(() => _currentIndex = index);
},
),
body: IndexedStack(
index: _currentIndex,
children: [
ChannelList(),
UserMentionsPage(),
],
),
);
}
StreamSubscription<int>? badgeListener;
@override
void initState() {
if (!kIsWeb) {
badgeListener = StreamChat.of(context)
.client
.state
.totalUnreadCountStream
.listen((count) {
if (count > 0) {
FlutterAppBadger.updateBadgeCount(count);
} else {
FlutterAppBadger.removeBadge();
}
});
}
super.initState();
}
@override
void dispose() {
badgeListener?.cancel();
super.dispose();
}
}
class LeftDrawer extends StatelessWidget {
const LeftDrawer({
Key? key,
required this.user,
}) : super(key: key);
final User user;
@override
Widget build(BuildContext context) {
return Drawer(
child: Container(
color: StreamChatTheme.of(context).colorTheme.barsBg,
child: SafeArea(
child: Padding(
padding: EdgeInsets.only(
top: MediaQuery.of(context).viewPadding.top + 8,
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(
bottom: 20.0,
left: 8,
),
child: Row(
children: [
StreamUserAvatar(
user: user,
showOnlineStatus: false,
constraints: BoxConstraints.tight(Size.fromRadius(20)),
),
Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Text(
user.name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
ListTile(
leading: StreamSvgIcon.penWrite(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(.5),
),
onTap: () {
Navigator.popAndPushNamed(
context,
Routes.NEW_CHAT,
);
},
title: Text(
AppLocalizations.of(context).newDirectMessage,
style: TextStyle(
fontSize: 14.5,
),
),
),
ListTile(
leading: StreamSvgIcon.contacts(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(.5),
),
onTap: () {
Navigator.popAndPushNamed(
context,
Routes.NEW_GROUP_CHAT,
);
},
title: Text(
AppLocalizations.of(context).newGroup,
style: TextStyle(
fontSize: 14.5,
),
),
),
Expanded(
child: Container(
alignment: Alignment.bottomCenter,
child: ListTile(
onTap: () async {
Navigator.pop(context);
if (!kIsWeb) {
final secureStorage = FlutterSecureStorage();
await secureStorage.deleteAll();
}
final client = StreamChat.of(context).client;
client.disconnectUser();
await client.dispose();
await Navigator.of(
context,
rootNavigator: true,
).pushNamedAndRemoveUntil(
Routes.CHOOSE_USER,
ModalRoute.withName(Routes.CHOOSE_USER),
);
},
leading: StreamSvgIcon.user(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(.5),
),
title: Text(
AppLocalizations.of(context).signOut,
style: TextStyle(
fontSize: 14.5,
),
),
trailing: IconButton(
icon: StreamSvgIcon.iconMoon(
size: 24,
),
color: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
onPressed: () async {
final sp = await StreamingSharedPreferences.instance;
sp.setInt(
'theme',
Theme.of(context).brightness == Brightness.dark
? 1
: -1,
);
},
),
),
),
),
],
),
),
),
),
);
}
}
@@ -0,0 +1,229 @@
import 'package:example/utils/localizations.dart';
import 'package:example/routes/routes.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart';
import 'channel_page.dart';
class ChannelMediaDisplayScreen extends StatefulWidget {
final StreamMessageThemeData messageTheme;
const ChannelMediaDisplayScreen({
Key? key,
required this.messageTheme,
}) : super(key: key);
@override
State<ChannelMediaDisplayScreen> createState() =>
_ChannelMediaDisplayScreenState();
}
class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
final Map<String?, VideoPlayerController?> controllerCache = {};
late final controller = StreamMessageSearchListController(
client: StreamChat.of(context).client,
filter: Filter.in_(
'cid',
[StreamChannel.of(context).channel.cid!],
),
messageFilter: Filter.in_(
'attachments.type',
['image', 'video'],
),
sort: [
SortOption(
'created_at',
direction: SortOption.ASC,
),
],
limit: 20,
);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
appBar: AppBar(
elevation: 1,
centerTitle: true,
title: Text(
AppLocalizations.of(context).photosAndVideos,
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.textHighEmphasis,
fontSize: 16.0,
),
),
leading: StreamBackButton(),
backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
),
body: ValueListenableBuilder(
valueListenable: controller,
builder: (BuildContext context,
PagedValue<String, GetMessageResponse> value, Widget? child) {
return value.when(
(items, nextPageKey, error) {
if (items.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
StreamSvgIcon.pictures(
size: 136.0,
color: StreamChatTheme.of(context).colorTheme.disabled,
),
SizedBox(height: 16.0),
Text(
AppLocalizations.of(context).noMedia,
style: TextStyle(
fontSize: 14.0,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis,
),
),
SizedBox(height: 8.0),
Text(
AppLocalizations.of(context)
.photosOrVideosWillAppearHere,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.0,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
),
),
],
),
);
}
final media = <_AssetPackage>[];
for (var item in value.asSuccess.items) {
item.message.attachments
.where((e) =>
(e.type == 'image' || e.type == 'video') &&
e.ogScrapeUrl == null)
.forEach((e) {
VideoPlayerController? controller;
if (e.type == 'video') {
var cachedController = controllerCache[e.assetUrl];
if (cachedController == null) {
controller = VideoPlayerController.network(e.assetUrl!);
controller.initialize();
controllerCache[e.assetUrl] = controller;
} else {
controller = cachedController;
}
}
media.add(_AssetPackage(e, item.message, controller));
});
}
return LazyLoadScrollView(
onEndOfPage: () async {
if (nextPageKey != null) {
controller.loadMore(nextPageKey);
}
},
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: StreamFullScreenMedia(
mediaAttachmentPackages: media
.map(
(e) => StreamAttachmentPackage(
attachment: e.attachment,
message: e.message,
),
)
.toList(),
startIndex: position,
userName: media[position].message.user!.name,
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,
),
);
},
),
),
),
);
},
child: media[position].attachment.type == 'image'
? IgnorePointer(
child: StreamImageAttachment(
attachment: media[position].attachment,
message: media[position].message,
showTitle: false,
messageTheme: widget.messageTheme,
),
)
: VideoPlayer(media[position].videoPlayer!),
),
);
},
itemCount: media.length,
),
);
},
loading: () => Center(
child: const CircularProgressIndicator(),
),
error: (_) => Offstage(),
);
},
),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
void initState() {
controller.doInitialLoad();
super.initState();
}
}
class _AssetPackage {
Attachment attachment;
Message message;
VideoPlayerController? videoPlayer;
_AssetPackage(this.attachment, this.message, this.videoPlayer);
}
@@ -0,0 +1,193 @@
import 'package:collection/collection.dart';
import 'package:example/routes/routes.dart';
import 'package:example/pages/thread_page.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'chat_info_screen.dart';
import 'group_info_screen.dart';
class ChannelPageArgs {
final Channel? channel;
final Message? initialMessage;
const ChannelPageArgs({
this.channel,
this.initialMessage,
});
}
class ChannelPage extends StatefulWidget {
final int? initialScrollIndex;
final double? initialAlignment;
final bool highlightInitialMessage;
const ChannelPage({
Key? key,
this.initialScrollIndex,
this.initialAlignment,
this.highlightInitialMessage = false,
}) : super(key: key);
@override
_ChannelPageState createState() => _ChannelPageState();
}
class _ChannelPageState extends State<ChannelPage> {
FocusNode? _focusNode;
StreamMessageInputController _messageInputController =
StreamMessageInputController();
@override
void initState() {
_focusNode = FocusNode();
super.initState();
}
@override
void dispose() {
_focusNode!.dispose();
super.dispose();
}
void _reply(Message message) {
_messageInputController.quotedMessage = message;
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_focusNode!.requestFocus();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
appBar: StreamChannelHeader(
showTypingIndicator: false,
onImageTap: () async {
var channel = StreamChannel.of(context).channel;
if (channel.memberCount == 2 && channel.isDistinct) {
final currentUser = StreamChat.of(context).currentUser;
final otherUser = channel.state!.members.firstWhereOrNull(
(element) => element.user!.id != currentUser!.id,
);
if (otherUser != null) {
final pop = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
child: ChatInfoScreen(
messageTheme: StreamChatTheme.of(context).ownMessageTheme,
user: otherUser.user,
),
channel: channel,
),
),
);
if (pop == true) {
Navigator.pop(context);
}
}
} else {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
child: GroupInfoScreen(
messageTheme: StreamChatTheme.of(context).ownMessageTheme,
),
channel: channel,
),
),
);
}
},
),
body: Column(
children: <Widget>[
Expanded(
child: Stack(
children: <Widget>[
StreamMessageListView(
initialScrollIndex: widget.initialScrollIndex,
initialAlignment: widget.initialAlignment,
highlightInitialMessage: widget.highlightInitialMessage,
onMessageSwiped: _reply,
messageFilter: defaultFilter,
messageBuilder: (context, details, messages, defaultMessage) {
return defaultMessage.copyWith(
onReplyTap: _reply,
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,
),
);
},
deletedBottomRowBuilder: (context, message) {
return const StreamVisibleFootnote();
},
);
},
threadBuilder: (_, parentMessage) {
return ThreadPage(parent: parentMessage!);
},
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
alignment: Alignment.centerLeft,
color: StreamChatTheme.of(context)
.colorTheme
.appBg
.withOpacity(.9),
child: StreamTypingIndicator(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis),
),
),
),
],
),
),
StreamMessageInput(
focusNode: _focusNode,
messageInputController: _messageInputController,
),
],
),
);
}
bool defaultFilter(Message m) {
var _currentUser = StreamChat.of(context).currentUser;
final isMyMessage = m.user?.id == _currentUser?.id;
final isDeletedOrShadowed = m.isDeleted == true || m.shadowed == true;
if (isDeletedOrShadowed && !isMyMessage) return false;
return true;
}
}
@@ -0,0 +1,598 @@
import 'package:example/pages/channel_file_display_screen.dart';
import 'package:example/utils/localizations.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:example/pages/channel_media_display_screen.dart';
import 'package:example/pages/pinned_messages_screen.dart';
/// Detail screen for a 1:1 chat correspondence
class ChatInfoScreen extends StatefulWidget {
/// User in consideration
final User? user;
final StreamMessageThemeData messageTheme;
const ChatInfoScreen({
Key? key,
required this.messageTheme,
this.user,
}) : super(key: key);
@override
_ChatInfoScreenState createState() => _ChatInfoScreenState();
}
class _ChatInfoScreenState extends State<ChatInfoScreen> {
ValueNotifier<bool?> mutedBool = ValueNotifier(false);
@override
void initState() {
super.initState();
mutedBool = ValueNotifier(StreamChannel.of(context).channel.isMuted);
}
@override
Widget build(BuildContext context) {
final channel = StreamChannel.of(context).channel;
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
body: ListView(
children: [
_buildUserHeader(),
Container(
height: 8.0,
color: StreamChatTheme.of(context).colorTheme.disabled,
),
_buildOptionListTiles(),
Container(
height: 8.0,
color: StreamChatTheme.of(context).colorTheme.disabled,
),
if (channel.ownCapabilities.contains(PermissionType.deleteChannel))
_buildDeleteListTile(),
],
),
);
}
Widget _buildUserHeader() {
return Material(
color: StreamChatTheme.of(context).colorTheme.appBg,
child: SafeArea(
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: StreamUserAvatar(
user: widget.user!,
constraints: BoxConstraints.tightFor(
width: 72.0,
height: 72.0,
),
borderRadius: BorderRadius.circular(36.0),
showOnlineStatus: false,
),
),
Text(
widget.user!.name,
style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold),
),
SizedBox(height: 7.0),
_buildConnectedTitleState(),
SizedBox(height: 15.0),
StreamOptionListTile(
title: '@${widget.user!.id}',
tileColor: StreamChatTheme.of(context).colorTheme.appBg,
trailing: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
widget.user!.name,
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
fontSize: 16.0),
),
),
onTap: () {},
),
],
),
Positioned(
top: 0,
left: 0,
width: 58,
child: StreamBackButton(),
),
],
),
),
);
}
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.textHighEmphasis.withOpacity(0.5),
// ),
// trailing: CupertinoSwitch(
// value: true,
// onChanged: (val) {},
// ),
// onTap: () {},
// ),
StreamBuilder<bool>(
stream: StreamChannel.of(context).channel.isMutedStream,
builder: (context, snapshot) {
mutedBool.value = snapshot.data;
return StreamOptionListTile(
tileColor: StreamChatTheme.of(context).colorTheme.appBg,
title: AppLocalizations.of(context).muteUser,
titleTextStyle: StreamChatTheme.of(context).textTheme.body,
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 22.0),
child: StreamSvgIcon.mute(
size: 24.0,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
),
),
trailing: snapshot.data == null
? CircularProgressIndicator()
: ValueListenableBuilder<bool?>(
valueListenable: mutedBool,
builder: (context, value, _) {
return CupertinoSwitch(
value: value!,
onChanged: (val) {
mutedBool.value = 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.textHighEmphasis.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: () {},
// ),
StreamOptionListTile(
title: AppLocalizations.of(context).pinnedMessages,
tileColor: StreamChatTheme.of(context).colorTheme.appBg,
titleTextStyle: StreamChatTheme.of(context).textTheme.body,
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 22.0),
child: StreamSvgIcon.pin(
size: 24.0,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
),
),
trailing: StreamSvgIcon.right(
color: StreamChatTheme.of(context).colorTheme.textLowEmphasis,
),
onTap: () {
final channel = StreamChannel.of(context).channel;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: PinnedMessagesScreen(),
),
),
);
},
),
StreamOptionListTile(
title: AppLocalizations.of(context).photosAndVideos,
tileColor: StreamChatTheme.of(context).colorTheme.appBg,
titleTextStyle: StreamChatTheme.of(context).textTheme.body,
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: StreamSvgIcon.pictures(
size: 36.0,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
),
),
trailing: StreamSvgIcon.right(
color: StreamChatTheme.of(context).colorTheme.textLowEmphasis,
),
onTap: () {
final channel = StreamChannel.of(context).channel;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: ChannelMediaDisplayScreen(
messageTheme: widget.messageTheme,
),
),
),
);
},
),
StreamOptionListTile(
title: AppLocalizations.of(context).files,
tileColor: StreamChatTheme.of(context).colorTheme.appBg,
titleTextStyle: StreamChatTheme.of(context).textTheme.body,
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18.0),
child: StreamSvgIcon.files(
size: 32.0,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
),
),
trailing: StreamSvgIcon.right(
color: StreamChatTheme.of(context).colorTheme.textLowEmphasis,
),
onTap: () {
final channel = StreamChannel.of(context).channel;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: ChannelFileDisplayScreen(
messageTheme: widget.messageTheme,
),
),
),
);
},
),
StreamOptionListTile(
title: AppLocalizations.of(context).sharedGroups,
tileColor: StreamChatTheme.of(context).colorTheme.appBg,
titleTextStyle: StreamChatTheme.of(context).textTheme.body,
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 22.0),
child: StreamSvgIcon.iconGroup(
size: 24.0,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
),
),
trailing: StreamSvgIcon.right(
color: StreamChatTheme.of(context).colorTheme.textLowEmphasis,
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => _SharedGroupsScreen(
StreamChat.of(context).currentUser, widget.user)));
},
),
],
);
}
Widget _buildDeleteListTile() {
return StreamOptionListTile(
title: 'Delete Conversation',
tileColor: StreamChatTheme.of(context).colorTheme.appBg,
titleTextStyle: StreamChatTheme.of(context).textTheme.body.copyWith(
color: StreamChatTheme.of(context).colorTheme.accentError,
),
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 22.0),
child: StreamSvgIcon.delete(
color: StreamChatTheme.of(context).colorTheme.accentError,
size: 24.0,
),
),
onTap: () {
_showDeleteDialog();
},
titleColor: StreamChatTheme.of(context).colorTheme.accentError,
);
}
void _showDeleteDialog() async {
final res = await showConfirmationBottomSheet(
context,
title: AppLocalizations.of(context).deleteConversationTitle,
okText: AppLocalizations.of(context).delete.toUpperCase(),
question: AppLocalizations.of(context).deleteConversationAreYouSure,
cancelText: AppLocalizations.of(context).cancel.toUpperCase(),
icon: StreamSvgIcon.delete(
color: StreamChatTheme.of(context).colorTheme.accentError,
),
);
var channel = StreamChannel.of(context).channel;
if (res == true) {
await channel.delete().then((value) {
Navigator.pop(context);
Navigator.pop(context);
});
}
}
Widget _buildConnectedTitleState() {
var alternativeWidget;
final otherMember = widget.user;
if (otherMember != null) {
if (otherMember.online) {
alternativeWidget = Text(
AppLocalizations.of(context).online,
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5)),
);
} else {
alternativeWidget = Text(
'${AppLocalizations.of(context).lastSeen} ${Jiffy(otherMember.lastActive).fromNow()}',
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.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: 24,
height: 12,
),
child: Material(
shape: CircleBorder(),
color: StreamChatTheme.of(context).colorTheme.accentInfo,
),
),
color: StreamChatTheme.of(context).colorTheme.barsBg,
),
alternativeWidget,
if (widget.user!.online)
SizedBox(
width: 24.0,
),
],
);
}
}
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.appBg,
appBar: AppBar(
elevation: 1,
centerTitle: true,
title: Text(
AppLocalizations.of(context).sharedGroups,
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.textHighEmphasis,
fontSize: 16.0),
),
leading: StreamBackButton(),
backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
),
body: StreamBuilder<List<Channel>>(
stream: chat.client.queryChannels(
filter: Filter.and([
Filter.in_('members', [widget.otherUser!.id]),
Filter.in_('members', [widget.mainUser!.id]),
]),
),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.data!.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
StreamSvgIcon.message(
size: 136.0,
color: StreamChatTheme.of(context).colorTheme.disabled,
),
SizedBox(height: 16.0),
Text(
AppLocalizations.of(context).noSharedGroups,
style: TextStyle(
fontSize: 14.0,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis,
),
),
SizedBox(height: 8.0),
Text(
AppLocalizations.of(context).groupSharedWithUserAppearHere,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.0,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
),
),
],
),
);
}
final channels = snapshot.data!
.where((c) =>
c.state!.members.any((m) =>
m.userId != widget.mainUser!.id &&
m.userId != widget.otherUser!.id) ||
!c.isDistinct)
.toList();
return ListView.builder(
itemCount: channels.length,
itemBuilder: (context, position) {
return StreamChannel(
channel: channels[position],
child: _buildListTile(channels[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).currentUser!.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'] as String;
}
return Column(
children: [
Expanded(
child: Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: StreamChannelAvatar(
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} ${AppLocalizations.of(context).members.toLowerCase()}',
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5)),
),
)
],
),
),
Container(
height: 1.0,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(.08),
),
],
);
}),
);
}
}
@@ -0,0 +1,202 @@
import 'package:example/app.dart';
import 'package:example/utils/app_config.dart';
import 'package:example/pages/home_page.dart';
import 'package:example/utils/localizations.dart';
import 'package:example/widgets/stream_version.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import '../routes/routes.dart';
const kStreamApiKey = 'STREAM_API_KEY';
const kStreamUserId = 'STREAM_USER_ID';
const kStreamToken = 'STREAM_TOKEN';
class ChooseUserPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final users = defaultUsers;
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(
top: 34,
bottom: 20,
),
child: Center(
child: SvgPicture.asset(
'assets/logo.svg',
height: 40,
color: StreamChatTheme.of(context).colorTheme.accentPrimary,
),
),
),
Padding(
padding: const EdgeInsets.only(bottom: 13.0),
child: Text(
AppLocalizations.of(context).welcomeToStreamChat,
style: StreamChatTheme.of(context).textTheme.title,
),
),
Text(
'${AppLocalizations.of(context).selectUserToTryFlutterSDK}:',
style: StreamChatTheme.of(context).textTheme.body,
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(top: 32),
child: ListView.separated(
separatorBuilder: (context, i) {
return Container(
height: 1,
color: StreamChatTheme.of(context).colorTheme.borders,
);
},
itemCount: users.length + 1,
itemBuilder: (context, i) {
return [
...users.entries.map((entry) {
final token = entry.key;
final user = entry.value;
return ListTile(
visualDensity: VisualDensity.compact,
onTap: () async {
showDialog(
barrierDismissible: false,
context: context,
barrierColor: StreamChatTheme.of(context)
.colorTheme
.overlay,
builder: (context) => Center(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: StreamChatTheme.of(context)
.colorTheme
.barsBg,
),
height: 100,
width: 100,
child: Center(
child: CircularProgressIndicator(),
),
),
),
);
final client = StreamChatClient(
kDefaultStreamApiKey,
logLevel: Level.INFO,
)..chatPersistenceClient = chatPersistentClient;
await client.connectUser(
user,
token,
);
if (!kIsWeb) {
final secureStorage = FlutterSecureStorage();
secureStorage.write(
key: kStreamApiKey,
value: kDefaultStreamApiKey,
);
secureStorage.write(
key: kStreamUserId,
value: user.id,
);
secureStorage.write(
key: kStreamToken,
value: token,
);
}
Navigator.pushNamedAndRemoveUntil(
context,
Routes.HOME,
ModalRoute.withName(Routes.HOME),
arguments: HomePageArgs(client),
);
},
leading: StreamUserAvatar(
user: user,
constraints: BoxConstraints.tight(
Size.fromRadius(20),
),
),
title: Text(
user.name,
style:
StreamChatTheme.of(context).textTheme.bodyBold,
),
subtitle: Text(
AppLocalizations.of(context).streamTestAccount,
style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
),
),
trailing: StreamSvgIcon.arrowRight(
color: StreamChatTheme.of(context)
.colorTheme
.accentPrimary,
),
);
}),
ListTile(
onTap: () {
Navigator.pushNamed(context, Routes.ADVANCED_OPTIONS);
},
leading: CircleAvatar(
child: StreamSvgIcon.settings(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis,
),
backgroundColor:
StreamChatTheme.of(context).colorTheme.borders,
),
title: Text(
AppLocalizations.of(context).advancedOptions,
style: StreamChatTheme.of(context).textTheme.bodyBold,
),
subtitle: Text(
AppLocalizations.of(context).customSettings,
style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
),
),
trailing: SvgPicture.asset(
'assets/icon_arrow_right.svg',
height: 24,
width: 24,
clipBehavior: Clip.none,
),
),
][i];
},
),
),
),
StreamVersion(),
],
),
),
);
}
}
@@ -0,0 +1,338 @@
import 'package:example/utils/localizations.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'channel_page.dart';
import '../routes/routes.dart';
class GroupChatDetailsScreen extends StatefulWidget {
final List<User>? selectedUsers;
const GroupChatDetailsScreen({
Key? key,
required this.selectedUsers,
}) : super(key: key);
@override
_GroupChatDetailsScreenState createState() => _GroupChatDetailsScreenState();
}
class _GroupChatDetailsScreenState extends State<GroupChatDetailsScreen> {
final _selectedUsers = <User>[];
TextEditingController? _groupNameController;
bool _isGroupNameEmpty = true;
int get _totalUsers => _selectedUsers.length;
void _groupNameListener() {
final name = _groupNameController!.text;
if (mounted) {
setState(() {
_isGroupNameEmpty = name.isEmpty;
});
}
}
@override
void initState() {
super.initState();
_selectedUsers.addAll(widget.selectedUsers!);
_groupNameController = TextEditingController()
..addListener(_groupNameListener);
}
@override
void dispose() {
_groupNameController?.removeListener(_groupNameListener);
_groupNameController?.clear();
_groupNameController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
Navigator.pop(context, _selectedUsers);
return false;
},
child: Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
appBar: AppBar(
elevation: 1,
backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
leading: const StreamBackButton(),
title: Text(
AppLocalizations.of(context).nameOfGroupChat,
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.textHighEmphasis,
fontSize: 16,
),
),
centerTitle: true,
bottom: PreferredSize(
preferredSize: Size.fromHeight(kToolbarHeight),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 16),
child: Row(
children: [
Text(
AppLocalizations.of(context).name.toUpperCase(),
style: TextStyle(
fontSize: 12,
color: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
),
),
SizedBox(width: 16),
Expanded(
child: TextField(
controller: _groupNameController,
decoration: InputDecoration(
isDense: true,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
contentPadding: const EdgeInsets.all(0),
hintText:
AppLocalizations.of(context).chooseAGroupChatName,
hintStyle: TextStyle(
fontSize: 14,
color: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
),
),
),
),
],
),
),
),
actions: [
StreamNeumorphicButton(
child: IconButton(
padding: const EdgeInsets.all(0),
icon: StreamSvgIcon.check(
size: 24,
color: _isGroupNameEmpty
? StreamChatTheme.of(context).colorTheme.textLowEmphasis
: StreamChatTheme.of(context).colorTheme.accentPrimary,
),
onPressed: _isGroupNameEmpty
? null
: () async {
try {
final groupName = _groupNameController!.text;
final client = StreamChat.of(context).client;
final channel = client.channel('messaging',
id: Uuid().v4(),
extraData: {
'members': [
client.state.currentUser!.id,
..._selectedUsers.map((e) => e.id),
],
'name': groupName,
});
await channel.watch();
Navigator.pushNamedAndRemoveUntil(
context,
Routes.CHANNEL_PAGE,
ModalRoute.withName(Routes.CHANNEL_LIST_PAGE),
arguments: ChannelPageArgs(channel: channel),
);
} catch (err) {
_showErrorAlert();
}
},
),
),
],
),
body: StreamConnectionStatusBuilder(
statusBuilder: (context, status) {
String statusString = '';
bool showStatus = true;
switch (status) {
case ConnectionStatus.connected:
statusString = AppLocalizations.of(context).connected;
showStatus = false;
break;
case ConnectionStatus.connecting:
statusString = AppLocalizations.of(context).reconnecting;
break;
case ConnectionStatus.disconnected:
statusString = AppLocalizations.of(context).disconnected;
break;
}
return StreamInfoTile(
showMessage: showStatus,
tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter,
message: statusString,
child: Column(
children: [
Container(
width: double.maxFinite,
decoration: BoxDecoration(
gradient:
StreamChatTheme.of(context).colorTheme.bgGradient,
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
child: Text(
'$_totalUsers ${_totalUsers > 1 ? AppLocalizations.of(context).members : AppLocalizations.of(context).member}',
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
),
),
),
),
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: (_) => FocusScope.of(context).unfocus(),
child: ListView.separated(
itemCount: _selectedUsers.length + 1,
separatorBuilder: (_, __) => Container(
height: 1,
color: StreamChatTheme.of(context).colorTheme.borders,
),
itemBuilder: (_, index) {
if (index == _selectedUsers.length) {
return Container(
height: 1,
color: StreamChatTheme.of(context)
.colorTheme
.borders,
);
}
final user = _selectedUsers[index];
return ListTile(
key: ObjectKey(user),
leading: StreamUserAvatar(
user: user,
constraints: BoxConstraints.tightFor(
width: 40,
height: 40,
),
),
title: Text(
user.name,
style: TextStyle(fontWeight: FontWeight.bold),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
trailing: IconButton(
icon: Icon(
Icons.clear_rounded,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis,
),
padding: const EdgeInsets.all(0),
splashRadius: 24,
onPressed: () {
setState(() {
_selectedUsers.remove(user);
});
if (_selectedUsers.isEmpty) {
Navigator.pop(context, _selectedUsers);
}
},
),
);
},
),
),
),
],
),
);
},
),
),
);
}
void _showErrorAlert() {
showModalBottomSheet(
useRootNavigator: false,
backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
context: context,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0),
)),
builder: (context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 26.0,
),
StreamSvgIcon.error(
color: StreamChatTheme.of(context).colorTheme.accentError,
size: 24.0,
),
SizedBox(
height: 26.0,
),
Text(
AppLocalizations.of(context).somethingWentWrongErrorMessage,
style: StreamChatTheme.of(context).textTheme.headlineBold,
),
SizedBox(
height: 7.0,
),
Text(AppLocalizations.of(context).operationCouldNotBeCompleted),
SizedBox(
height: 36.0,
),
Container(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(.08),
height: 1.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
child: Text(
AppLocalizations.of(context).ok,
style: StreamChatTheme.of(context)
.textTheme
.bodyBold
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.accentPrimary),
),
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
],
);
},
);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,115 @@
import 'dart:async';
import 'package:example/pages/channel_page.dart';
import 'package:example/utils/notifications_service.dart';
import 'package:example/routes/app_routes.dart';
import 'package:example/routes/routes.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class MyObserver extends NavigatorObserver {
Route? currentRoute;
late final StreamSubscription _subscription;
MyObserver(
StreamChatClient client,
GlobalKey<NavigatorState> navigatorKey,
) {
_subscription = client
.on(
EventType.messageNew,
EventType.notificationMessageNew,
)
.listen((event) {
if (event.message?.user?.id == client.state.currentUser?.id) {
return;
}
final channelId = event.channelId;
if (currentRoute?.settings.name == Routes.CHANNEL_PAGE) {
final args = currentRoute?.settings.arguments as ChannelPageArgs;
if (args.channel?.id == channelId) {
return;
}
}
showLocalNotification(
event,
client.state.currentUser!.id,
navigatorKey.currentState!.context,
);
});
}
@override
void didPop(Route route, Route? previousRoute) {
currentRoute = route;
}
@override
void didPush(Route route, Route? previousRoute) {
currentRoute = route;
}
@override
void didRemove(Route route, Route? previousRoute) {
currentRoute = route;
}
@override
void didReplace({Route? newRoute, Route? oldRoute}) {
currentRoute = newRoute;
}
void dispose() {
_subscription.cancel();
}
}
class HomePageArgs {
final StreamChatClient chatClient;
HomePageArgs(this.chatClient);
}
class HomePage extends StatefulWidget {
HomePage({
Key? key,
required this.chatClient,
}) : super(key: key);
final StreamChatClient chatClient;
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey();
MyObserver? _observer;
@override
Widget build(BuildContext context) {
return StreamChat(
client: widget.chatClient,
child: WillPopScope(
onWillPop: () async {
final canPop = await _navigatorKey.currentState?.maybePop() ?? false;
return !canPop;
},
child: Navigator(
key: _navigatorKey,
onGenerateRoute: AppRoutes.generateRoute,
initialRoute: Routes.CHANNEL_LIST_PAGE,
observers: [_observer!],
),
),
);
}
@override
void didChangeDependencies() {
_observer?.dispose();
_observer = MyObserver(widget.chatClient, _navigatorKey);
super.didChangeDependencies();
}
}
@@ -0,0 +1,426 @@
import 'dart:async';
import 'package:example/utils/localizations.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'channel_page.dart';
import '../widgets/chips_input_text_field.dart';
import '../routes/routes.dart';
class NewChatScreen extends StatefulWidget {
@override
_NewChatScreenState createState() => _NewChatScreenState();
}
class _NewChatScreenState extends State<NewChatScreen> {
final _chipInputTextFieldStateKey =
GlobalKey<ChipInputTextFieldState<User>>();
late TextEditingController _controller;
late final userListController = StreamUserListController(
client: StreamChat.of(context).client,
limit: 25,
filter: Filter.and([
Filter.notEqual('id', StreamChat.of(context).currentUser!.id),
]),
sort: [
SortOption(
'name',
direction: 1,
),
],
);
ChipInputTextFieldState? get _chipInputTextFieldState =>
_chipInputTextFieldStateKey.currentState;
String _userNameQuery = '';
final _selectedUsers = <User>{};
final _searchFocusNode = FocusNode();
final _messageInputFocusNode = FocusNode();
bool _isSearchActive = false;
Channel? channel;
Timer? _debounce;
bool _showUserList = true;
void _userNameListener() {
if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 350), () {
if (mounted)
setState(() {
_userNameQuery = _controller.text;
_isSearchActive = _userNameQuery.isNotEmpty;
});
userListController.filter = Filter.and([
if (_userNameQuery.isNotEmpty)
Filter.autoComplete('name', _userNameQuery),
Filter.notEqual('id', StreamChat.of(context).currentUser!.id),
]);
userListController.doInitialLoad();
});
}
@override
void initState() {
super.initState();
channel = StreamChat.of(context).client.channel('messaging');
_controller = TextEditingController()..addListener(_userNameListener);
_searchFocusNode.addListener(() async {
if (_searchFocusNode.hasFocus && !_showUserList) {
setState(() {
_showUserList = true;
});
}
});
_messageInputFocusNode.addListener(() async {
if (_messageInputFocusNode.hasFocus && _selectedUsers.isNotEmpty) {
final chatState = StreamChat.of(context);
final res = await chatState.client.queryChannelsOnline(
state: false,
watch: false,
filter: Filter.raw(value: {
'members': [
..._selectedUsers.map((e) => e.id),
chatState.currentUser!.id,
],
'distinct': true,
}),
messageLimit: 0,
paginationParams: PaginationParams(
limit: 1,
),
);
final _channelExisted = res.length == 1;
if (_channelExisted) {
channel = res.first;
await channel!.watch();
} else {
channel = chatState.client.channel(
'messaging',
extraData: {
'members': [
..._selectedUsers.map((e) => e.id),
chatState.currentUser!.id,
],
},
);
}
setState(() {
_showUserList = false;
});
}
});
}
@override
void dispose() {
_searchFocusNode.dispose();
_messageInputFocusNode.dispose();
_controller.clear();
_controller.removeListener(_userNameListener);
_controller.dispose();
userListController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
appBar: AppBar(
elevation: 0,
backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
leading: const StreamBackButton(),
title: Text(
AppLocalizations.of(context).newChat,
style: StreamChatTheme.of(context).textTheme.headlineBold.copyWith(
color: StreamChatTheme.of(context).colorTheme.textHighEmphasis),
),
centerTitle: true,
),
body: StreamConnectionStatusBuilder(
statusBuilder: (context, status) {
String statusString = '';
bool showStatus = true;
switch (status) {
case ConnectionStatus.connected:
statusString = AppLocalizations.of(context).connected;
showStatus = false;
break;
case ConnectionStatus.connecting:
statusString = AppLocalizations.of(context).reconnecting;
break;
case ConnectionStatus.disconnected:
statusString = AppLocalizations.of(context).disconnected;
break;
}
return StreamInfoTile(
showMessage: showStatus,
tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter,
message: statusString,
child: StreamChannel(
showLoading: false,
channel: channel!,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ChipsInputTextField<User>(
key: _chipInputTextFieldStateKey,
controller: _controller,
focusNode: _searchFocusNode,
hint: AppLocalizations.of(context).typeANameHint,
chipBuilder: (context, user) {
return GestureDetector(
onTap: () {
_chipInputTextFieldState?.removeItem(user);
_searchFocusNode.requestFocus();
},
child: Stack(
alignment: AlignmentDirectional.centerStart,
children: [
Container(
decoration: BoxDecoration(
color: StreamChatTheme.of(context)
.colorTheme
.disabled,
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.only(left: 24),
child: Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 12, 4),
child: Text(
user.name,
maxLines: 1,
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis,
),
),
),
),
Container(
foregroundDecoration: BoxDecoration(
color: StreamChatTheme.of(context)
.colorTheme
.overlay,
shape: BoxShape.circle,
),
child: StreamUserAvatar(
showOnlineStatus: false,
user: user,
constraints: BoxConstraints.tightFor(
height: 24,
width: 24,
),
),
),
StreamSvgIcon.close(),
],
),
);
},
onChipAdded: (user) {
setState(() => _selectedUsers.add(user));
},
onChipRemoved: (user) {
setState(() => _selectedUsers.remove(user));
},
),
if (!_isSearchActive && !_selectedUsers.isNotEmpty)
Container(
child: InkWell(
onTap: () {
Navigator.pushNamed(
context,
Routes.NEW_GROUP_CHAT,
);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
StreamNeumorphicButton(
child: Center(
child: StreamSvgIcon.contacts(
color: StreamChatTheme.of(context)
.colorTheme
.accentPrimary,
size: 24,
),
),
),
SizedBox(width: 8),
Text(
AppLocalizations.of(context).createAGroup,
style: StreamChatTheme.of(context)
.textTheme
.bodyBold,
),
],
),
),
),
),
if (_showUserList)
Container(
width: double.maxFinite,
decoration: BoxDecoration(
gradient:
StreamChatTheme.of(context).colorTheme.bgGradient,
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
child: Text(
_isSearchActive
? '${AppLocalizations.of(context).matchesFor} "$_userNameQuery"'
: AppLocalizations.of(context).onThePlatorm,
style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(.5))),
),
),
Expanded(
child: _showUserList
? GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: (_) => FocusScope.of(context).unfocus(),
child: StreamUserListView(
controller: userListController,
// groupAlphabetically:
// _isSearchActive ? false : true,
onUserTap: (user) {
_controller.clear();
if (!_selectedUsers.contains(user)) {
_chipInputTextFieldState
?..addItem(user)
..pauseItemAddition();
} else {
_chipInputTextFieldState!.removeItem(user);
}
},
itemBuilder: (
context,
users,
index,
defaultWidget,
) {
return defaultWidget.copyWith(
selected:
_selectedUsers.contains(users[index]),
);
},
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(
AppLocalizations.of(context)
.noUserMatchesTheseKeywords,
style: StreamChatTheme.of(
context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme
.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(.5)),
),
],
),
),
),
);
},
);
},
),
)
: FutureBuilder<bool>(
future: channel!.initialized,
builder: (context, snapshot) {
if (snapshot.data == true) {
return StreamMessageListView();
}
return Center(
child: Text(
AppLocalizations.of(context).noChatsHereYet,
style: TextStyle(
fontSize: 12,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(.5),
),
),
);
},
),
),
StreamMessageInput(
focusNode: _messageInputFocusNode,
preMessageSending: (message) async {
await channel!.watch();
return message;
},
onMessageSent: (m) {
Navigator.pushNamedAndRemoveUntil(
context,
Routes.CHANNEL_PAGE,
ModalRoute.withName(Routes.CHANNEL_LIST_PAGE),
arguments: ChannelPageArgs(channel: channel),
);
},
),
],
),
),
);
},
),
);
}
}
@@ -0,0 +1,347 @@
import 'dart:async';
import 'package:example/utils/localizations.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import '../routes/routes.dart';
import '../widgets/search_text_field.dart';
class NewGroupChatScreen extends StatefulWidget {
@override
_NewGroupChatScreenState createState() => _NewGroupChatScreenState();
}
class _NewGroupChatScreenState extends State<NewGroupChatScreen> {
TextEditingController? _controller;
String _userNameQuery = '';
final _selectedUsers = <User>{};
bool _isSearchActive = false;
Timer? _debounce;
late final userListController = StreamUserListController(
client: StreamChat.of(context).client,
sort: [
SortOption(
'name',
direction: 1,
),
],
limit: 25,
filter: Filter.and([
Filter.notEqual('id', StreamChat.of(context).currentUser!.id),
]),
);
void _userNameListener() {
if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 350), () {
if (mounted) {
setState(() {
_userNameQuery = _controller!.text;
_isSearchActive = _userNameQuery.isNotEmpty;
});
userListController.filter = Filter.and([
if (_userNameQuery.isNotEmpty)
Filter.autoComplete('name', _userNameQuery),
Filter.notEqual('id', StreamChat.of(context).currentUser!.id),
]);
userListController.doInitialLoad();
}
});
}
@override
void initState() {
super.initState();
_controller = TextEditingController()..addListener(_userNameListener);
}
@override
void dispose() {
_controller?.clear();
_controller?.removeListener(_userNameListener);
_controller?.dispose();
userListController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
appBar: AppBar(
elevation: 1,
backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
leading: const StreamBackButton(),
title: Text(
AppLocalizations.of(context).addGroupMembers,
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.textHighEmphasis,
fontSize: 16,
),
),
centerTitle: true,
actions: [
if (_selectedUsers.isNotEmpty)
IconButton(
icon: StreamSvgIcon.arrowRight(
color: StreamChatTheme.of(context).colorTheme.accentPrimary,
),
onPressed: () async {
final updatedList = await Navigator.pushNamed(
context,
Routes.NEW_GROUP_CHAT_DETAILS,
arguments: _selectedUsers.toList(growable: false),
);
if (updatedList != null) {
setState(() {
_selectedUsers
..clear()
..addAll(updatedList as Iterable<User>);
});
}
},
)
],
),
body: StreamConnectionStatusBuilder(
statusBuilder: (context, status) {
String statusString = '';
bool showStatus = true;
switch (status) {
case ConnectionStatus.connected:
statusString = AppLocalizations.of(context).connected;
showStatus = false;
break;
case ConnectionStatus.connecting:
statusString = AppLocalizations.of(context).reconnecting;
break;
case ConnectionStatus.disconnected:
statusString = AppLocalizations.of(context).disconnected;
break;
}
return StreamInfoTile(
showMessage: showStatus,
tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter,
message: statusString,
child: NestedScrollView(
floatHeaderSlivers: true,
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
return <Widget>[
SliverToBoxAdapter(
child: SearchTextField(
controller: _controller,
hintText: AppLocalizations.of(context).search,
),
),
if (_selectedUsers.isNotEmpty)
SliverToBoxAdapter(
child: Container(
height: 104,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: _selectedUsers.length,
padding: const EdgeInsets.all(8),
separatorBuilder: (_, __) => SizedBox(width: 16),
itemBuilder: (_, index) {
final user = _selectedUsers.elementAt(index);
return Column(
children: [
Stack(
children: [
StreamUserAvatar(
onlineIndicatorAlignment:
Alignment(0.9, 0.9),
user: user,
showOnlineStatus: true,
borderRadius: BorderRadius.circular(32),
constraints: BoxConstraints.tightFor(
height: 64,
width: 64,
),
),
Positioned(
top: -4,
right: -4,
child: GestureDetector(
onTap: () {
if (_selectedUsers.contains(user)) {
setState(() =>
_selectedUsers.remove(user));
}
},
child: Container(
decoration: BoxDecoration(
color: StreamChatTheme.of(context)
.colorTheme
.appBg,
shape: BoxShape.circle,
border: Border.all(
color: StreamChatTheme.of(context)
.colorTheme
.appBg,
),
),
child: StreamSvgIcon.close(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis,
size: 24,
),
),
),
)
],
),
SizedBox(height: 4),
Text(
user.name.split(' ')[0],
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
],
);
},
),
),
),
SliverPersistentHeader(
pinned: true,
delegate: _HeaderDelegate(
height: 30,
child: Container(
width: double.maxFinite,
decoration: BoxDecoration(
gradient:
StreamChatTheme.of(context).colorTheme.bgGradient,
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
child: Text(
_isSearchActive
? '${AppLocalizations.of(context).matchesFor} \"$_userNameQuery\"'
: AppLocalizations.of(context).onThePlatorm,
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
),
),
),
),
),
),
];
},
body: GestureDetector(
behavior: HitTestBehavior.opaque,
onPanDown: (_) => FocusScope.of(context).unfocus(),
child: StreamUserListView(
controller: userListController,
itemBuilder: (context, items, index, defaultWidget) {
return defaultWidget.copyWith(
selected: _selectedUsers.contains(items[index]),
);
},
onUserTap: (user) {
if (!_selectedUsers.contains(user)) {
setState(() {
_selectedUsers.add(user);
});
} else {
setState(() {
_selectedUsers.remove(user);
});
}
},
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: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
),
),
Text(
AppLocalizations.of(context)
.noUserMatchesTheseKeywords,
style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
),
),
],
),
),
),
);
},
);
},
),
),
),
);
},
),
);
}
}
class _HeaderDelegate extends SliverPersistentHeaderDelegate {
final Widget child;
final double height;
const _HeaderDelegate({
required this.child,
required this.height,
});
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return Container(
color: StreamChatTheme.of(context).colorTheme.barsBg,
child: child,
);
}
@override
double get maxExtent => height;
@override
double get minExtent => height;
@override
bool shouldRebuild(_HeaderDelegate oldDelegate) => true;
}
@@ -0,0 +1,130 @@
import 'package:example/utils/localizations.dart';
import 'package:example/routes/routes.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'channel_page.dart';
class PinnedMessagesScreen extends StatefulWidget {
@override
State<PinnedMessagesScreen> createState() => _PinnedMessagesScreenState();
}
class _PinnedMessagesScreenState extends State<PinnedMessagesScreen> {
late final controller = StreamMessageSearchListController(
client: StreamChat.of(context).client,
filter: Filter.in_(
'cid',
[StreamChannel.of(context).channel.cid!],
),
messageFilter: Filter.equal(
'pinned',
true,
),
sort: [
SortOption(
'created_at',
direction: SortOption.ASC,
),
],
limit: 20,
);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
appBar: AppBar(
elevation: 1,
centerTitle: true,
title: Text(
AppLocalizations.of(context).pinnedMessages,
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.textHighEmphasis,
fontSize: 16.0,
),
),
leading: StreamBackButton(),
backgroundColor: StreamChatTheme.of(context).colorTheme.barsBg,
),
body: StreamMessageSearchListView(
controller: controller,
emptyBuilder: (_) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
StreamSvgIcon.pin(
size: 136.0,
color: StreamChatTheme.of(context).colorTheme.disabled,
),
SizedBox(height: 16.0),
Text(
AppLocalizations.of(context).noPinnedItems,
style: TextStyle(
fontSize: 17.0,
color:
StreamChatTheme.of(context).colorTheme.textHighEmphasis,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8.0),
RichText(
textAlign: TextAlign.center,
text: TextSpan(children: [
TextSpan(
text: '${AppLocalizations.of(context).longPressMessage} ',
style: TextStyle(
fontSize: 14.0,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
),
),
TextSpan(
text: AppLocalizations.of(context).pinToConversation,
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.bold,
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
),
),
]),
),
],
),
);
},
onMessageTap: (messageResponse) async {
final client = StreamChat.of(context).client;
final message = messageResponse.message;
final channel = client.channel(
messageResponse.channel!.type,
id: messageResponse.channel!.id,
);
if (channel.state == null) {
await channel.watch();
}
Navigator.pushNamed(
context,
Routes.CHANNEL_PAGE,
arguments: ChannelPageArgs(
channel: channel,
initialMessage: message,
),
);
},
),
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
@@ -0,0 +1,121 @@
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
mixin SplashScreenStateMixin<T extends StatefulWidget> on State<T>
implements TickerProvider {
late Animation<double> animation, scaleAnimation;
late AnimationController _animationController, _scaleAnimationController;
late Animation<Color?> colorAnimation;
bool animationCompleted = false;
void _createAnimations() {
_scaleAnimationController = AnimationController(
vsync: this,
value: 0,
duration: Duration(
milliseconds: 500,
),
);
scaleAnimation = Tween(
begin: 1.0,
end: 1.5,
).animate(CurvedAnimation(
parent: _scaleAnimationController,
curve: Curves.easeInOutBack,
));
_animationController = AnimationController(
vsync: this,
duration: Duration(
milliseconds: 1000,
),
);
animation = Tween(
begin: 0.0,
end: 1000.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
colorAnimation = ColorTween(
begin: Color(0xff005FFF),
end: Color(0xff005FFF),
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
colorAnimation = ColorTween(
begin: Color(0xff005FFF),
end: Colors.transparent,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
}
void forwardAnimations() {
_scaleAnimationController.forward().whenComplete(() {
_animationController.forward();
});
}
Widget buildAnimation() => Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
AnimatedBuilder(
animation: scaleAnimation,
builder: (context, _) {
return Transform.scale(
scale: scaleAnimation.value,
child: AnimatedBuilder(
animation: colorAnimation,
builder: (context, snapshot) {
return Container(
alignment: Alignment.center,
constraints: BoxConstraints.expand(),
color: colorAnimation.value,
child: !_animationController.isAnimating
? Lottie.asset(
'assets/floating_boat.json',
alignment: Alignment.center,
)
: SizedBox(),
);
}),
);
},
),
AnimatedBuilder(
animation: animation,
builder: (context, snapshot) {
return Transform.scale(
scale: animation.value,
child: Container(
width: 1.0,
height: 1.0,
decoration: BoxDecoration(
color: Colors.white
.withOpacity(1 - _animationController.value),
shape: BoxShape.circle,
),
),
);
},
),
],
);
@override
void initState() {
_createAnimations();
_animationController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
setState(() {
animationCompleted = true;
});
}
});
super.initState();
}
}
@@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class ThreadPage extends StatefulWidget {
final Message parent;
final int? initialScrollIndex;
final double? initialAlignment;
ThreadPage({
Key? key,
required this.parent,
this.initialScrollIndex,
this.initialAlignment,
}) : super(key: key);
@override
_ThreadPageState createState() => _ThreadPageState();
}
class _ThreadPageState extends State<ThreadPage> {
FocusNode _focusNode = FocusNode();
late StreamMessageInputController _messageInputController;
@override
void initState() {
super.initState();
_messageInputController = StreamMessageInputController(
message: Message(
parentId: widget.parent.id,
));
}
@override
void dispose() {
_focusNode.dispose();
super.dispose();
}
void _reply(Message message) {
_messageInputController.quotedMessage = message;
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_focusNode.requestFocus();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
appBar: StreamThreadHeader(
parent: widget.parent,
),
body: Column(
children: <Widget>[
Expanded(
child: StreamMessageListView(
parentMessage: widget.parent,
initialScrollIndex: widget.initialScrollIndex,
initialAlignment: widget.initialAlignment,
onMessageSwiped: _reply,
messageFilter: defaultFilter,
showScrollToBottom: false,
highlightInitialMessage: true,
messageBuilder: (context, details, messages, defaultMessage) {
return defaultMessage.copyWith(
onReplyTap: _reply,
deletedBottomRowBuilder: (context, message) {
return const StreamVisibleFootnote();
},
);
},
),
),
if (widget.parent.type != 'deleted')
StreamMessageInput(
focusNode: _focusNode,
messageInputController: _messageInputController,
),
],
),
);
}
bool defaultFilter(Message m) {
var _currentUser = StreamChat.of(context).currentUser;
final isMyMessage = m.user?.id == _currentUser?.id;
final isDeletedOrShadowed = m.isDeleted == true || m.shadowed == true;
if (isDeletedOrShadowed && !isMyMessage) return false;
return true;
}
}
@@ -0,0 +1,98 @@
import 'package:example/utils/localizations.dart';
import 'package:example/routes/routes.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'channel_page.dart';
class UserMentionsPage extends StatefulWidget {
@override
State<UserMentionsPage> createState() => _UserMentionsPageState();
}
class _UserMentionsPageState extends State<UserMentionsPage> {
late final controller = StreamMessageSearchListController(
client: StreamChat.of(context).client,
filter: Filter.in_('members', [StreamChat.of(context).currentUser!.id]),
messageFilter: Filter.custom(
operator: r'$contains',
key: 'mentioned_users.id',
value: StreamChat.of(context).currentUser!.id,
),
sort: [
SortOption(
'created_at',
direction: SortOption.ASC,
),
],
limit: 20,
);
@override
Widget build(BuildContext context) {
return StreamMessageSearchListView(
controller: controller,
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.mentions(
size: 96,
color:
StreamChatTheme.of(context).colorTheme.disabled,
),
),
Text(
AppLocalizations.of(context).noMentionsExistYet,
style:
StreamChatTheme.of(context).textTheme.body.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.textLowEmphasis,
),
),
],
),
),
),
);
},
);
},
onMessageTap: (messageResponse) async {
final client = StreamChat.of(context).client;
final message = messageResponse.message;
final channel = client.channel(
messageResponse.channel!.type,
id: messageResponse.channel!.id,
);
if (channel.state == null) {
await channel.watch();
}
Navigator.pushNamed(
context,
Routes.CHANNEL_PAGE,
arguments: ChannelPageArgs(
channel: channel,
initialMessage: message,
),
);
},
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}