style: provide folder structure
This commit is contained in:
@@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user