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,320 @@
import 'dart:async';
import 'package:example/utils/localizations.dart';
import 'package:example/routes/routes.dart';
import 'package:example/widgets/search_text_field.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import '../pages/channel_page.dart';
import '../pages/chat_info_screen.dart';
import '../pages/group_info_screen.dart';
class ChannelList extends StatefulWidget {
@override
_ChannelList createState() => _ChannelList();
}
class _ChannelList extends State<ChannelList> {
ScrollController _scrollController = ScrollController();
late StreamMessageSearchListController _messageSearchListController =
StreamMessageSearchListController(
client: StreamChat.of(context).client,
filter: Filter.in_('members', [StreamChat.of(context).currentUser!.id]),
limit: 5,
searchQuery: '',
sort: [
SortOption(
'created_at',
direction: SortOption.ASC,
),
],
);
TextEditingController? _controller;
bool _isSearchActive = false;
Timer? _debounce;
void _channelQueryListener() {
if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 350), () {
if (mounted) {
_messageSearchListController.searchQuery = _controller!.text;
setState(() {
_isSearchActive = _controller!.text.isNotEmpty;
});
if (_isSearchActive) _messageSearchListController.doInitialLoad();
}
});
}
late final _channelListController = StreamChannelListController(
client: StreamChat.of(context).client,
filter: Filter.in_(
'members',
[StreamChat.of(context).currentUser!.id],
),
presence: true,
limit: 30,
);
@override
void initState() {
super.initState();
_controller = TextEditingController()..addListener(_channelQueryListener);
}
@override
void dispose() {
_controller?.removeListener(_channelQueryListener);
_controller?.dispose();
_scrollController.dispose();
_channelListController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
if (_isSearchActive) {
_controller!.clear();
setState(() => _isSearchActive = false);
return false;
}
return true;
},
child: NotificationListener<ScrollUpdateNotification>(
onNotification: (ScrollNotification scrollInfo) {
if (_scrollController.position.userScrollDirection ==
ScrollDirection.reverse) {
FocusScope.of(context).unfocus();
}
return true;
},
child: NestedScrollView(
controller: _scrollController,
floatHeaderSlivers: false,
headerSliverBuilder: (_, __) => [
SliverToBoxAdapter(
child: SearchTextField(
controller: _controller,
showCloseButton: _isSearchActive,
hintText: AppLocalizations.of(context).search,
),
),
],
body: _isSearchActive
? StreamMessageSearchListView(
controller: _messageSearchListController,
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).noResults,
),
],
),
),
),
);
},
);
},
itemBuilder: (
context,
messageResponses,
index,
defaultWidget,
) {
return defaultWidget.copyWith(
onTap: () async {
final messageResponse = messageResponses[index];
FocusScope.of(context).requestFocus(FocusNode());
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,
),
);
},
);
},
)
: SlidableAutoCloseBehavior(
closeWhenOpened: true,
child: RefreshIndicator(
onRefresh: _channelListController.refresh,
child: StreamChannelListView(
controller: _channelListController,
itemBuilder: (context, channels, index, defaultWidget) {
final chatTheme = StreamChatTheme.of(context);
final backgroundColor = chatTheme.colorTheme.inputBg;
final channel = channels[index];
final canDeleteChannel = channel.ownCapabilities
.contains(PermissionType.deleteChannel);
return Slidable(
groupTag: 'channels-actions',
endActionPane: ActionPane(
extentRatio: canDeleteChannel ? 0.40 : 0.20,
motion: const BehindMotion(),
children: [
CustomSlidableAction(
child: Icon(Icons.more_horiz),
backgroundColor: backgroundColor,
onPressed: (_) {
showChannelInfoModalBottomSheet(
context: context,
channel: channel,
onViewInfoTap: () {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
final isOneToOne =
channel.memberCount == 2 &&
channel.isDistinct;
return StreamChannel(
channel: channel,
child: isOneToOne
? ChatInfoScreen(
messageTheme: chatTheme
.ownMessageTheme,
user: channel
.state!.members
.where((m) =>
m.userId !=
channel
.client
.state
.currentUser!
.id)
.first
.user,
)
: GroupInfoScreen(
messageTheme: chatTheme
.ownMessageTheme,
),
);
},
),
);
},
);
},
),
if (canDeleteChannel)
CustomSlidableAction(
backgroundColor: backgroundColor,
child: StreamSvgIcon.delete(
color: chatTheme.colorTheme.accentError,
),
onPressed: (_) async {
final res =
await showConfirmationBottomSheet(
context,
title: 'Delete Conversation',
question:
'Are you sure you want to delete this conversation?',
okText: 'Delete',
cancelText: 'Cancel',
icon: StreamSvgIcon.delete(
color: chatTheme.colorTheme.accentError,
),
);
if (res == true) {
await _channelListController
.deleteChannel(channel);
}
},
),
],
),
child: defaultWidget,
);
},
onChannelTap: (channel) {
Navigator.pushNamed(
context,
Routes.CHANNEL_PAGE,
arguments: ChannelPageArgs(
channel: channel,
),
);
},
emptyBuilder: (_) {
return Center(
child: Padding(
padding: const EdgeInsets.all(8),
child: StreamScrollViewEmptyWidget(
emptyIcon: StreamSvgIcon.message(
size: 148,
color: StreamChatTheme.of(context)
.colorTheme
.disabled,
),
emptyTitle: TextButton(
onPressed: () {
Navigator.pushNamed(
context,
Routes.NEW_CHAT,
);
},
child: Text(
'Start a chat',
style: StreamChatTheme.of(context)
.textTheme
.bodyBold
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.accentPrimary,
),
),
),
),
),
);
},
),
),
),
),
),
);
}
}
@@ -0,0 +1,167 @@
import 'package:example/utils/localizations.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
typedef ChipBuilder<T> = Widget Function(BuildContext context, T chip);
typedef OnChipAdded<T> = void Function(T chip);
typedef OnChipRemoved<T> = void Function(T chip);
class ChipsInputTextField<T> extends StatefulWidget {
final TextEditingController? controller;
final FocusNode? focusNode;
final ValueChanged<String>? onInputChanged;
final ChipBuilder<T> chipBuilder;
final OnChipAdded<T>? onChipAdded;
final OnChipRemoved<T>? onChipRemoved;
final String hint;
const ChipsInputTextField({
Key? key,
required this.chipBuilder,
required this.controller,
this.onInputChanged,
this.focusNode,
this.onChipAdded,
this.onChipRemoved,
this.hint = 'Type a name',
}) : super(key: key);
@override
ChipInputTextFieldState<T> createState() => ChipInputTextFieldState<T>();
}
class ChipInputTextFieldState<T> extends State<ChipsInputTextField<T>> {
final _chips = <T>{};
bool _pauseItemAddition = false;
void addItem(T item) {
setState(() => _chips.add(item));
if (widget.onChipAdded != null) widget.onChipAdded!(item);
}
void removeItem(T item) {
setState(() {
_chips.remove(item);
if (_chips.isEmpty) resumeItemAddition();
});
if (widget.onChipRemoved != null) widget.onChipRemoved!(item);
}
void pauseItemAddition() {
if (!_pauseItemAddition) {
setState(() => _pauseItemAddition = true);
}
widget.focusNode?.unfocus();
}
void resumeItemAddition() {
if (_pauseItemAddition) {
setState(() => _pauseItemAddition = false);
}
widget.focusNode?.requestFocus();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _pauseItemAddition ? resumeItemAddition : null,
child: Material(
elevation: 1,
color: StreamChatTheme.of(context).colorTheme.barsBg,
child: Container(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
child: Row(
children: [
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Text(
'${AppLocalizations.of(context).to.toUpperCase()}:',
style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(.5)),
),
),
SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Wrap(
spacing: 8.0,
runSpacing: 4.0,
children: _chips.map((item) {
return widget.chipBuilder(context, item);
}).toList(),
),
if (!_pauseItemAddition)
TextField(
controller: widget.controller,
onChanged: widget.onInputChanged,
focusNode: widget.focusNode,
decoration: InputDecoration(
isDense: true,
border: InputBorder.none,
focusedBorder: InputBorder.none,
enabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
disabledBorder: InputBorder.none,
contentPadding: const EdgeInsets.only(top: 4.0),
hintText: widget.hint,
hintStyle: StreamChatTheme.of(context)
.textTheme
.body
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(.5)),
),
),
],
),
),
SizedBox(width: 12),
Align(
alignment: Alignment.bottomCenter,
child: IconButton(
icon: _chips.isEmpty
? StreamSvgIcon.user(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
size: 24,
)
: StreamSvgIcon.userAdd(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(0.5),
size: 24,
),
onPressed: resumeItemAddition,
alignment: Alignment.topRight,
visualDensity: VisualDensity.compact,
padding: const EdgeInsets.all(0),
splashRadius: 24,
constraints: BoxConstraints.tightFor(
height: 24,
width: 24,
),
),
),
],
),
),
),
),
);
}
}
@@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class SearchTextField extends StatelessWidget {
final TextEditingController? controller;
final ValueChanged<String>? onChanged;
final String hintText;
final VoidCallback? onTap;
final bool showCloseButton;
const SearchTextField({
Key? key,
required this.controller,
this.onChanged,
this.onTap,
this.hintText = 'Search',
this.showCloseButton = true,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
height: 36,
decoration: BoxDecoration(
color: StreamChatTheme.of(context).colorTheme.barsBg,
border: Border.all(
color: StreamChatTheme.of(context).colorTheme.borders,
),
borderRadius: BorderRadius.circular(24),
),
margin: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
child: Row(
children: [
Expanded(
child: TextField(
onTap: onTap,
controller: controller,
onChanged: onChanged,
decoration: InputDecoration(
prefixText: ' ',
prefixIconConstraints: BoxConstraints.tight(Size(40, 24)),
prefixIcon: Padding(
padding: const EdgeInsets.only(
left: 8,
right: 8,
),
child: StreamSvgIcon.search(
color:
StreamChatTheme.of(context).colorTheme.textHighEmphasis,
size: 24,
),
),
hintText: hintText,
hintStyle: StreamChatTheme.of(context).textTheme.body.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(.5)),
contentPadding: const EdgeInsets.all(0),
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(24),
),
),
),
),
if (showCloseButton)
Material(
color: Colors.transparent,
child: IconButton(
padding: const EdgeInsets.all(0),
icon: StreamSvgIcon.closeSmall(
color: Colors.grey,
),
splashRadius: 24,
onPressed: () {
if (controller!.text.isNotEmpty) {
Future.microtask(
() => [
controller!.clear(),
if (onChanged != null) onChanged!(''),
],
);
}
},
),
),
],
),
);
}
}
@@ -0,0 +1,40 @@
import 'package:example/utils/localizations.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:yaml/yaml.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class StreamVersion extends StatelessWidget {
const StreamVersion({
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 16),
alignment: Alignment.bottomCenter,
child: FutureBuilder<String>(
future: rootBundle.loadString('pubspec.lock'),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return SizedBox();
}
final pubspec = snapshot.data!;
final yaml = loadYaml(pubspec);
final streamChatDep =
yaml['packages']['stream_chat_flutter']['version'];
return Text(
'${AppLocalizations.of(context).streamSDK} v $streamChatDep',
style: TextStyle(
fontSize: 14,
color: StreamChatTheme.of(context).colorTheme.disabled,
),
);
},
),
);
}
}