Move Flutter widgets to packages directory

This commit is contained in:
Neevash Ramdial
2021-01-07 12:04:34 -04:00
parent ceed8e0440
commit 640f361513
305 changed files with 0 additions and 235 deletions
-39
View File
@@ -1,39 +0,0 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import '../stream_chat_flutter.dart';
class AttachmentError extends StatelessWidget {
final Attachment attachment;
final Size size;
const AttachmentError({
Key key,
@required this.attachment,
this.size,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (attachment.localUri != null) {
return Image.file(
File(attachment.localUri.path),
);
}
return Center(
child: Container(
width: size?.width,
height: size?.height ?? 200,
color: StreamChatTheme.of(context).colorTheme.accentRed.withOpacity(.1),
child: Center(
child: Icon(
Icons.error_outline,
color: StreamChatTheme.of(context).colorTheme.black,
),
),
),
);
}
}
-56
View File
@@ -1,56 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'stream_chat_theme.dart';
import 'utils.dart';
class AttachmentTitle extends StatelessWidget {
const AttachmentTitle({
Key key,
@required this.attachment,
@required this.messageTheme,
}) : super(key: key);
final MessageTheme messageTheme;
final Attachment attachment;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
if (attachment.titleLink != null) {
launchURL(context, attachment.titleLink);
}
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
attachment.title,
overflow: TextOverflow.ellipsis,
style: messageTheme.messageText.copyWith(
color: StreamChatTheme.of(context).colorTheme.accentBlue,
fontWeight: FontWeight.bold,
),
),
if (attachment.titleLink != null || attachment.ogScrapeUrl != null)
Text(
Uri.parse(attachment.titleLink ?? attachment.ogScrapeUrl)
.authority
.split('.')
.reversed
.take(2)
.toList()
.reversed
.join('.'),
style: messageTheme.messageText,
),
],
),
),
);
}
}
-53
View File
@@ -1,53 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/unread_indicator.dart';
import '../stream_chat_flutter.dart';
class StreamBackButton extends StatelessWidget {
const StreamBackButton({
Key key,
this.onPressed,
this.showUnreads = false,
}) : super(key: key);
final VoidCallback onPressed;
final bool showUnreads;
@override
Widget build(BuildContext context) {
return Stack(
children: [
Padding(
padding: const EdgeInsets.all(14.0),
child: RawMaterialButton(
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
elevation: 0,
highlightElevation: 0,
focusElevation: 0,
disabledElevation: 0,
hoverElevation: 0,
onPressed: () {
if (onPressed != null) {
onPressed();
} else {
Navigator.maybePop(context);
}
},
child: StreamSvgIcon.left(
size: 24,
color: StreamChatTheme.of(context).colorTheme.black,
),
),
),
if (showUnreads)
Positioned(
top: 7,
right: 7,
child: UnreadIndicator(),
),
],
);
}
}
-223
View File
@@ -1,223 +0,0 @@
import 'package:flutter/material.dart';
import '../stream_chat_flutter.dart';
import 'channel_info.dart';
import 'option_list_tile.dart';
class ChannelBottomSheet extends StatefulWidget {
final VoidCallback onViewInfoTap;
ChannelBottomSheet({this.onViewInfoTap});
@override
_ChannelBottomSheetState createState() => _ChannelBottomSheetState();
}
class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
@override
Widget build(BuildContext context) {
var channel = StreamChannel.of(context).channel;
var members = channel.state.members;
var userAsMember =
members.firstWhere((e) => e.user.id == StreamChat.of(context).user.id);
var isOwner = userAsMember.role == 'owner';
return Material(
color: StreamChatTheme.of(context).colorTheme.white,
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0),
),
),
child: ListView(
shrinkWrap: true,
children: [
SizedBox(
height: 24.0,
),
Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: ChannelName(
textStyle: StreamChatTheme.of(context).textTheme.headlineBold,
),
),
),
SizedBox(
height: 5.0,
),
Center(
child: ChannelInfo(
showTypingIndicator: false,
channel: StreamChannel.of(context).channel,
textStyle:
StreamChatTheme.of(context).channelPreviewTheme.subtitle,
),
),
SizedBox(
height: 17.0,
),
if (channel.isDistinct && channel.memberCount == 2)
Column(
children: [
UserAvatar(
user: members
.firstWhere((e) => e.user.id != userAsMember.user.id)
.user,
constraints: BoxConstraints(
maxHeight: 64.0,
maxWidth: 64.0,
),
borderRadius: BorderRadius.circular(32.0),
onlineIndicatorConstraints:
BoxConstraints.tight(Size(16.0, 16.0)),
),
SizedBox(
height: 6.0,
),
Text(
members
.firstWhere((e) => e.user.id != userAsMember.user.id)
.user
.name,
style: StreamChatTheme.of(context).textTheme.footnoteBold,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
if (!(channel.isDistinct && channel.memberCount == 2))
Container(
height: 94.0,
alignment: Alignment.center,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: members.length,
shrinkWrap: true,
itemBuilder: (context, index) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Column(
children: [
UserAvatar(
user: members[index].user,
constraints: BoxConstraints(
maxHeight: 64.0,
maxWidth: 64.0,
),
borderRadius: BorderRadius.circular(32.0),
),
SizedBox(
height: 6.0,
),
Text(
members[index].user.name,
style: StreamChatTheme.of(context)
.textTheme
.footnoteBold,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
);
},
),
),
SizedBox(
height: 24.0,
),
OptionListTile(
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: StreamSvgIcon.user(
color: StreamChatTheme.of(context).colorTheme.grey,
),
),
title: 'View Info',
onTap: widget.onViewInfoTap,
),
if (!channel.isDistinct)
OptionListTile(
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: StreamSvgIcon.userRemove(
color: StreamChatTheme.of(context).colorTheme.grey,
),
),
title: 'Leave Group',
onTap: () async {
_showLeaveDialog();
},
),
if (isOwner)
OptionListTile(
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: StreamSvgIcon.delete(
color: StreamChatTheme.of(context).colorTheme.accentRed,
),
),
title: 'Delete Conversation',
titleColor: StreamChatTheme.of(context).colorTheme.accentRed,
onTap: () async {
_showDeleteDialog();
},
),
OptionListTile(
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: StreamSvgIcon.close_small(
color: StreamChatTheme.of(context).colorTheme.grey,
),
),
title: 'Cancel',
onTap: () {
Navigator.pop(context);
},
),
],
),
);
}
void _showDeleteDialog() async {
final res = await showConfirmationDialog(
context,
title: 'Delete Conversation',
okText: 'DELETE',
question: 'Are you sure you want to delete this conversation?',
cancelText: 'CANCEL',
icon: StreamSvgIcon.delete(
color: StreamChatTheme.of(context).colorTheme.accentRed,
),
);
var channel = StreamChannel.of(context).channel;
if (res == true) {
await channel.delete();
Navigator.pop(context);
}
}
void _showLeaveDialog() async {
final res = await showConfirmationDialog(
context,
title: 'Leave conversation',
okText: 'LEAVE',
question: 'Are you sure you want to leave this conversation?',
cancelText: 'CANCEL',
icon: StreamSvgIcon.userRemove(
color: StreamChatTheme.of(context).colorTheme.accentRed,
),
);
var channel = StreamChannel.of(context).channel;
if (res == true) {
await channel.removeMembers([StreamChat.of(context).user.id]);
Navigator.pop(context);
}
}
}
-182
View File
@@ -1,182 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class ChannelFileDisplayScreen extends StatefulWidget {
/// The sorting used for the channels matching the filters.
/// Sorting is based on field and direction, multiple sorting options can be provided.
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
/// Direction can be ascending or descending.
final List<SortOption> sortOptions;
/// Pagination parameters
/// limit: the number of users to return (max is 30)
/// offset: the offset (max is 1000)
/// message_limit: how many messages should be included to each channel
final PaginationParams paginationParams;
/// The builder used when the file list is empty.
final WidgetBuilder emptyBuilder;
const ChannelFileDisplayScreen({
this.sortOptions,
this.paginationParams,
this.emptyBuilder,
});
@override
_ChannelFileDisplayScreenState createState() =>
_ChannelFileDisplayScreenState();
}
class _ChannelFileDisplayScreenState extends State<ChannelFileDisplayScreen> {
@override
void initState() {
super.initState();
final messageSearchBloc = MessageSearchBloc.of(context);
messageSearchBloc.search(
filter: {
'cid': {
r'$in': ['messaging:${StreamChannel.of(context).channel.id}']
}
},
messageFilter: {
'attachments.type': {
r'$in': ['file'],
},
},
sort: widget.sortOptions,
pagination: widget.paginationParams,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
appBar: AppBar(
brightness: Theme.of(context).brightness,
elevation: 1,
centerTitle: true,
title: Text(
'Files',
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.black,
fontSize: 16.0),
),
leading: Center(
child: InkWell(
onTap: () {
Navigator.of(context).pop();
},
child: Container(
child: StreamSvgIcon.left(
color: StreamChatTheme.of(context).colorTheme.black,
size: 24.0,
),
width: 24.0,
height: 24.0,
),
),
),
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
),
body: _buildMediaGrid(),
);
}
Widget _buildMediaGrid() {
final messageSearchBloc = MessageSearchBloc.of(context);
return StreamBuilder<List<GetMessageResponse>>(
builder: (context, snapshot) {
if (snapshot.data == null) {
return Center(
child: const CircularProgressIndicator(),
);
}
if (snapshot.data.isEmpty) {
if (widget.emptyBuilder != null) {
return widget.emptyBuilder(context);
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
StreamSvgIcon.files(
size: 136.0,
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
),
SizedBox(height: 16.0),
Text(
'No Files',
style: TextStyle(
fontSize: 14.0,
color: StreamChatTheme.of(context).colorTheme.black,
),
),
SizedBox(height: 8.0),
Text(
'Files sent in this chat will appear here',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.0,
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
),
),
],
),
);
}
final media = <Attachment, Message>{};
for (var item in snapshot.data) {
item.message.attachments.where((e) => e.type == 'file').forEach((e) {
media[e] = item.message;
});
}
return LazyLoadScrollView(
onEndOfPage: () => messageSearchBloc.search(
filter: {
'cid': {
r'$in': ['messaging:${StreamChannel.of(context).channel.id}']
}
},
messageFilter: {
'attachments.type': {
r'$in': ['file']
},
},
sort: widget.sortOptions,
pagination: widget.paginationParams.copyWith(
offset: messageSearchBloc.messageResponses?.length ?? 0,
),
),
child: ListView.builder(
itemBuilder: (context, position) {
var channel = StreamChannel.of(context).channel;
return Padding(
padding: const EdgeInsets.all(1.0),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: FileAttachment(
attachment: media.keys.toList()[position],
),
),
);
},
itemCount: media.length,
),
);
},
stream: messageSearchBloc.messagesStream,
);
}
}
-138
View File
@@ -1,138 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/back_button.dart';
import 'package:stream_chat_flutter/src/channel_info.dart';
import 'package:stream_chat_flutter/src/channel_name.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import '../stream_chat_flutter.dart';
import './channel_name.dart';
import 'channel_image.dart';
import 'stream_channel.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header_paint.png)
///
/// It shows the current [Channel] information.
///
/// ```dart
/// class MyApp extends StatelessWidget {
/// final Client client;
/// final Channel channel;
///
/// MyApp(this.client, this.channel);
///
/// @override
/// Widget build(BuildContext context) {
/// return MaterialApp(
/// home: StreamChat(
/// client: client,
/// child: StreamChannel(
/// channel: channel,
/// child: Scaffold(
/// appBar: ChannelHeader(),
/// ),
/// ),
/// ),
/// );
/// }
/// }
/// ```
///
/// Usually you would use this widget as an [AppBar] inside a [Scaffold].
/// However you can also use it as a normal widget.
///
/// Make sure to have a [StreamChannel] ancestor in order to provide the information about the channel.
/// Every part of the widget uses a [StreamBuilder] to render the channel information as soon as it updates.
///
/// By default the widget shows a backButton that calls [Navigator.pop].
/// You can disable this button using the [showBackButton] property of just override the behaviour
/// with [onBackPressed].
///
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property.
/// Modify it to change the widget appearance.
class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
/// True if this header shows the leading back button
final bool showBackButton;
/// Callback to call when pressing the back button.
/// By default it calls [Navigator.pop]
final VoidCallback onBackPressed;
/// Callback to call when the header is tapped.
final VoidCallback onTitleTap;
/// Callback to call when the image is tapped.
final VoidCallback onImageTap;
/// If true the typing indicator will be rendered if a user is typing
final bool showTypingIndicator;
/// Creates a channel header
ChannelHeader({
Key key,
this.showBackButton = true,
this.onBackPressed,
this.onTitleTap,
this.showTypingIndicator = true,
this.onImageTap,
}) : preferredSize = Size.fromHeight(kToolbarHeight),
super(key: key);
@override
Widget build(BuildContext context) {
final channel = StreamChannel.of(context).channel;
return AppBar(
brightness: Theme.of(context).brightness,
elevation: 1,
leading: showBackButton
? StreamBackButton(
onPressed: onBackPressed,
showUnreads: true,
)
: SizedBox(),
backgroundColor:
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
actions: <Widget>[
Padding(
padding: const EdgeInsets.only(right: 10.0),
child: Center(
child: ChannelImage(
onTap: onImageTap,
),
),
),
],
centerTitle: true,
title: InkWell(
onTap: onTitleTap,
child: Container(
height: preferredSize.height,
width: preferredSize.width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ChannelName(
textStyle: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.title,
),
SizedBox(height: 2),
ChannelInfo(
showTypingIndicator: showTypingIndicator,
channel: channel,
textStyle:
StreamChatTheme.of(context).channelPreviewTheme.subtitle,
),
],
),
),
),
);
}
@override
final Size preferredSize;
}
-214
View File
@@ -1,214 +0,0 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/group_image.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_image.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_image_paint.png)
///
/// It shows the current [Channel] image.
///
/// ```dart
/// class MyApp extends StatelessWidget {
/// final Client client;
/// final Channel channel;
///
/// MyApp(this.client, this.channel);
///
/// @override
/// Widget build(BuildContext context) {
/// return MaterialApp(
/// debugShowCheckedModeBanner: false,
/// home: StreamChat(
/// client: client,
/// child: StreamChannel(
/// channel: channel,
/// child: Center(
/// child: ChannelImage(
/// channel: channel,
/// ),
/// ),
/// ),
/// ),
/// );
/// }
/// }
/// ```
///
/// The widget uses a [StreamBuilder] to render the channel information image as soon as it updates.
///
/// By default the widget radius size is 40x40 pixels.
/// Set the property [constraints] to set a custom dimension.
///
/// The widget renders the ui based on the first ancestor of type [StreamChatTheme].
/// Modify it to change the widget appearance.
class ChannelImage extends StatelessWidget {
/// Instantiate a new ChannelImage
const ChannelImage({
Key key,
this.channel,
this.constraints,
this.onTap,
this.showOnlineStatus = true,
this.borderRadius,
this.selected = false,
this.selectionColor,
this.selectionThickness = 4,
}) : super(key: key);
final BorderRadius borderRadius;
/// The channel to show the image of
final Channel channel;
/// The diameter of the image
final BoxConstraints constraints;
/// The function called when the image is tapped
final VoidCallback onTap;
final bool showOnlineStatus;
final bool selected;
final Color selectionColor;
final double selectionThickness;
@override
Widget build(BuildContext context) {
final streamChat = StreamChat.of(context);
final channel = this.channel ?? StreamChannel.of(context).channel;
return StreamBuilder<Map<String, dynamic>>(
stream: channel.extraDataStream,
initialData: channel.extraData,
builder: (context, snapshot) {
String image;
if (snapshot.data?.containsKey('image') == true) {
image = snapshot.data['image'];
} else if (channel.state.members?.length == 2) {
final otherMember = channel.state.members
.firstWhere((member) => member.user.id != streamChat.user.id);
return StreamBuilder<User>(
stream: streamChat.client.state.usersStream
.map((users) => users[otherMember.userId]),
initialData: otherMember.user,
builder: (context, snapshot) {
return UserAvatar(
borderRadius: borderRadius,
user: snapshot.data ?? otherMember.user,
constraints: constraints ??
StreamChatTheme.of(context)
.channelPreviewTheme
.avatarTheme
.constraints,
onTap: onTap != null ? (_) => onTap() : null,
selected: selected,
selectionColor: selectionColor ??
StreamChatTheme.of(context).colorTheme.accentBlue,
selectionThickness: selectionThickness,
);
});
} else {
final images = channel.state.members
.where((member) =>
member.user.id != streamChat.user.id &&
member.user.extraData['image'] != null)
.take(4)
.map((e) => e.user.extraData['image'] as String)
.toList();
return GroupImage(
images: images,
borderRadius: borderRadius,
constraints: constraints ??
StreamChatTheme.of(context)
.channelPreviewTheme
.avatarTheme
.constraints,
onTap: onTap,
selected: selected,
selectionColor: selectionColor ??
StreamChatTheme.of(context).colorTheme.accentBlue,
selectionThickness: selectionThickness,
);
}
Widget child = ClipRRect(
borderRadius: borderRadius ??
StreamChatTheme.of(context)
.channelPreviewTheme
.avatarTheme
.borderRadius,
child: Container(
constraints: constraints ??
StreamChatTheme.of(context)
.channelPreviewTheme
.avatarTheme
.constraints,
decoration: BoxDecoration(
color: StreamChatTheme.of(context).colorTheme.accentBlue,
),
child: Stack(
alignment: Alignment.center,
fit: StackFit.expand,
children: <Widget>[
image != null
? CachedNetworkImage(
imageUrl: image,
errorWidget: (_, __, ___) {
return Center(
child: Text(
snapshot.data?.containsKey('name') ?? false
? snapshot.data['name'][0]
: '',
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.white,
fontWeight: FontWeight.bold,
),
),
);
},
fit: BoxFit.cover,
)
: StreamChatTheme.of(context)
.defaultChannelImage(context, channel),
Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
),
),
],
),
),
);
if (selected) {
child = ClipRRect(
borderRadius: (borderRadius ??
StreamChatTheme.of(context)
.ownMessageTheme
.avatarTheme
.borderRadius) +
BorderRadius.circular(selectionThickness),
child: Container(
constraints: constraints ??
StreamChatTheme.of(context)
.ownMessageTheme
.avatarTheme
.constraints,
color: selectionColor ??
StreamChatTheme.of(context).colorTheme.accentBlue,
child: Padding(
padding: EdgeInsets.all(selectionThickness),
child: child,
),
),
);
}
return child;
});
}
}
-144
View File
@@ -1,144 +0,0 @@
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class ChannelInfo extends StatelessWidget {
final Channel channel;
/// The style of the text displayed
final TextStyle textStyle;
/// If true the typing indicator will be rendered if a user is typing
final bool showTypingIndicator;
const ChannelInfo({
Key key,
@required this.channel,
this.textStyle,
this.showTypingIndicator = true,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final client = StreamChat.of(context).client;
return StreamBuilder<List<Member>>(
stream: channel.state.membersStream,
initialData: channel.state.members,
builder: (context, snapshot) {
return ValueListenableBuilder(
valueListenable: client.wsConnectionStatus,
builder: (context, status, child) {
switch (status) {
case ConnectionStatus.connected:
return _buildConnectedTitleState(context, snapshot.data);
case ConnectionStatus.connecting:
return _buildConnectingTitleState(context);
case ConnectionStatus.disconnected:
return _buildDisconnectedTitleState(context, client);
default:
return Offstage();
}
},
);
},
);
}
Widget _buildConnectedTitleState(BuildContext context, List<Member> members) {
var alternativeWidget;
if (channel.memberCount != null && channel.memberCount > 2) {
alternativeWidget = Text(
'${channel.memberCount} Members, ${channel.state.watcherCount} Online',
style: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.lastMessageAt,
);
} else {
final otherMember = members.firstWhere(
(element) => element.userId != StreamChat.of(context).user.id,
orElse: () => null,
);
if (otherMember != null) {
if (otherMember.user.online) {
alternativeWidget = Text(
'Online',
style: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.lastMessageAt,
);
} else {
alternativeWidget = Text(
'Last seen ${Jiffy(otherMember.user.lastActive).fromNow()}',
style: textStyle,
);
}
}
}
if (!showTypingIndicator) {
return alternativeWidget ?? Offstage();
}
return TypingIndicator(
alignment: Alignment.center,
alternativeWidget: alternativeWidget,
style: textStyle,
);
}
Widget _buildConnectingTitleState(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
height: 16,
width: 16,
child: Center(
child: CircularProgressIndicator(),
),
),
SizedBox(width: 10),
Text(
'Searching for Network',
style: textStyle,
),
],
);
}
Widget _buildDisconnectedTitleState(BuildContext context, Client client) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Offline...',
style: textStyle,
),
TextButton(
style: TextButton.styleFrom(
padding: const EdgeInsets.all(0),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity(
horizontal: VisualDensity.minimumDensity,
vertical: VisualDensity.minimumDensity,
),
),
onPressed: () async {
await client.disconnect();
return client.connect();
},
child: Text(
'Try Again',
style: textStyle.copyWith(
color: StreamChatTheme.of(context).colorTheme.accentBlue,
),
),
),
],
);
}
}
-218
View File
@@ -1,218 +0,0 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_neumorphic_button.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'stream_chat.dart';
typedef _TitleBuilder = Widget Function(
BuildContext context,
ConnectionStatus status,
Client client,
);
///
/// It shows the current [Client] status.
///
/// ```dart
/// class MyApp extends StatelessWidget {
/// final Client client;
///
/// MyApp(this.client);
///
/// @override
/// Widget build(BuildContext context) {
/// return MaterialApp(
/// home: StreamChat(
/// client: client,
/// child: Scaffold(
/// appBar: ChannelListHeader(),
/// ),
/// ),
/// );
/// }
/// }
/// ```
///
/// Usually you would use this widget as an [AppBar] inside a [Scaffold].
/// However you can also use it as a normal widget.
///
/// The widget by default uses the inherited [Client] to fetch information about the status.
/// However you can also pass your own [Client] if you don't have it in the widget tree.
///
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property.
/// Modify it to change the widget appearance.
class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
/// Instantiates a ChannelListHeader
const ChannelListHeader({
Key key,
this.client,
this.titleBuilder,
this.onUserAvatarTap,
this.onNewChatButtonTap,
}) : super(key: key);
/// Pass this if you don't have a [Client] in your widget tree.
final Client client;
/// Use this to build your own title as per different [ConnectionStatus]
final _TitleBuilder titleBuilder;
/// Callback to call when pressing the user avatar button.
/// By default it calls Scaffold.of(context).openDrawer()
final Function(User) onUserAvatarTap;
/// Callback to call when pressing the new chat button.
final VoidCallback onNewChatButtonTap;
@override
Widget build(BuildContext context) {
final _client = client ?? StreamChat.of(context).client;
final user = _client.state.user;
return AppBar(
brightness: Theme.of(context).brightness,
elevation: 1,
backgroundColor:
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
centerTitle: true,
leading: Center(
child: UserAvatar(
user: user,
showOnlineStatus: false,
onTap: onUserAvatarTap ?? (_) => Scaffold.of(context).openDrawer(),
borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor(
height: 40,
width: 40,
),
),
),
actions: [
StreamNeumorphicButton(
child: IconButton(
icon: ValueListenableBuilder<ConnectionStatus>(
valueListenable: _client.wsConnectionStatus,
builder: (context, status, child) {
var color;
switch (status) {
case ConnectionStatus.connected:
color = StreamChatTheme.of(context).colorTheme.accentBlue;
break;
case ConnectionStatus.connecting:
color = Colors.grey;
break;
case ConnectionStatus.disconnected:
color = Colors.grey;
break;
}
return SvgPicture.asset(
'svgs/icon_pen_write.svg',
package: 'stream_chat_flutter',
width: 24.0,
height: 24.0,
color: color,
);
},
),
onPressed: onNewChatButtonTap,
),
)
],
title: ValueListenableBuilder<ConnectionStatus>(
valueListenable: _client.wsConnectionStatus,
builder: (context, status, child) {
if (titleBuilder != null) {
return titleBuilder(context, status, _client);
}
switch (status) {
case ConnectionStatus.connected:
return _buildConnectedTitleState(context);
case ConnectionStatus.connecting:
return _buildConnectingTitleState(context);
case ConnectionStatus.disconnected:
return _buildDisconnectedTitleState(context, _client);
default:
return Offstage();
}
},
),
);
}
Widget _buildConnectedTitleState(BuildContext context) => Text(
'Stream Chat',
style: StreamChatTheme.of(context).textTheme.headlineBold.copyWith(
color: StreamChatTheme.of(context).colorTheme.black,
),
);
Widget _buildConnectingTitleState(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
height: 16,
width: 16,
child: Center(
child: CircularProgressIndicator(),
),
),
SizedBox(width: 10),
Text(
'Searching for Network',
style: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.title
.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
);
}
Widget _buildDisconnectedTitleState(BuildContext context, Client client) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Offline...',
style: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.title
.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
TextButton(
onPressed: () async {
await client.disconnect();
return client.connect();
},
child: Text(
'Try Again',
style: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.title
.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
color: StreamChatTheme.of(context).colorTheme.accentBlue,
),
),
),
],
);
}
@override
Size get preferredSize => Size.fromHeight(kToolbarHeight);
}
-829
View File
@@ -1,829 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:shimmer/shimmer.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/channels_bloc.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/utils.dart';
import '../stream_chat_flutter.dart';
import 'channel_bottom_sheet.dart';
import 'channel_preview.dart';
import 'stream_channel.dart';
import 'stream_chat.dart';
/// Callback called when tapping on a channel
typedef ChannelTapCallback = void Function(Channel, Widget);
/// Builder used to create a custom [ChannelPreview] from a [Channel]
typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel);
typedef ViewInfoCallback = void Function(Channel);
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_list_view.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_list_view_paint.png)
///
/// It shows the list of current channels.
///
/// ```dart
/// class ChannelListPage extends StatelessWidget {
/// @override
/// Widget build(BuildContext context) {
/// return Scaffold(
/// body: ChannelListView(
/// filter: {
/// 'members': {
/// '\$in': [StreamChat.of(context).user.id],
/// }
/// },
/// sort: [SortOption('last_message_at')],
/// pagination: PaginationParams(
/// limit: 20,
/// ),
/// channelWidget: ChannelPage(),
/// ),
/// );
/// }
/// }
/// ```
///
///
/// Make sure to have a [StreamChat] ancestor in order to provide the information about the channels.
/// The widget uses a [ListView.custom] to render the list of channels.
///
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme].
/// Modify it to change the widget appearance.
class ChannelListView extends StatefulWidget {
/// Instantiate a new ChannelListView
ChannelListView({
Key key,
this.filter,
this.options,
this.sort,
this.pagination,
this.onChannelTap,
this.onChannelLongPress,
this.channelWidget,
this.channelPreviewBuilder,
this.separatorBuilder,
this.errorBuilder,
this.emptyBuilder,
this.onImageTap,
this.onStartChatPressed,
this.swipeToAction = false,
this.pullToRefresh = true,
this.crossAxisCount = 1,
this.padding,
this.selectedChannels = const [],
this.onViewInfoTap,
}) : super(key: key);
/// The builder that will be used in case of error
final Widget Function(Error error) errorBuilder;
/// If true a default swipe to action behaviour will be added to this widget
final bool swipeToAction;
/// The builder used when the channel list is empty.
final WidgetBuilder emptyBuilder;
/// The query filters to use.
/// You can query on any of the custom fields you've defined on the [Channel].
/// You can also filter other built-in channel fields.
final Map<String, dynamic> filter;
/// Query channels options.
///
/// state: if true returns the Channel state
/// watch: if true listen to changes to this Channel in real time.
final Map<String, dynamic> options;
/// The sorting used for the channels matching the filters.
/// Sorting is based on field and direction, multiple sorting options can be provided.
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
/// Direction can be ascending or descending.
final List<SortOption> sort;
/// Pagination parameters
/// limit: the number of channels to return (max is 30)
/// offset: the offset (max is 1000)
/// message_limit: how many messages should be included to each channel
final PaginationParams pagination;
/// Function called when tapping on a channel
/// By default it calls [Navigator.push] building a [MaterialPageRoute]
/// with the widget [channelWidget] as child.
final ChannelTapCallback onChannelTap;
/// Function called when long pressing on a channel
final Function(Channel) onChannelLongPress;
/// Widget used when opening a channel
final Widget channelWidget;
/// Builder used to create a custom channel preview
final ChannelPreviewBuilder channelPreviewBuilder;
/// Builder used to create a custom item separator
final Function(BuildContext, int) separatorBuilder;
/// The function called when the image is tapped
final Function(Channel) onImageTap;
/// Set it to false to disable the pull-to-refresh widget
final bool pullToRefresh;
/// Callback used in the default empty list widget
final VoidCallback onStartChatPressed;
/// The number of children in the cross axis.
final int crossAxisCount;
/// The amount of space by which to inset the children.
final EdgeInsetsGeometry padding;
final List<Channel> selectedChannels;
final ViewInfoCallback onViewInfoTap;
@override
_ChannelListViewState createState() => _ChannelListViewState();
}
class _ChannelListViewState extends State<ChannelListView>
with WidgetsBindingObserver {
final ScrollController _scrollController = ScrollController();
final SlidableController _slideController = SlidableController();
@override
Widget build(BuildContext context) {
final channelsBloc = ChannelsBloc.of(context);
if (!widget.pullToRefresh) {
return _buildListView(channelsBloc);
}
return RefreshIndicator(
onRefresh: () async {
return channelsBloc.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination,
options: widget.options,
);
},
child: _buildListView(channelsBloc),
);
}
StreamBuilder<List<Channel>> _buildListView(
ChannelsBlocState channelsBlocState,
) {
return StreamBuilder<List<Channel>>(
stream: channelsBlocState.channelsStream,
builder: (context, snapshot) {
var child;
if (snapshot.hasError) {
child = _buildErrorWidget(
snapshot,
context,
channelsBlocState,
);
} else if (!snapshot.hasData) {
child = _buildLoadingWidget();
} else {
final channels = snapshot.data;
if (channels.isEmpty && widget.emptyBuilder != null) {
child = widget.emptyBuilder(context);
}
if (channels.isEmpty && widget.emptyBuilder == null) {
child = LayoutBuilder(
builder: (context, viewportConstraints) {
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: Stack(
children: [
ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: StreamSvgIcon.message(
size: 136,
color: StreamChatTheme.of(context)
.colorTheme
.greyGainsboro,
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Lets start chatting!',
style: StreamChatTheme.of(context)
.textTheme
.headline,
),
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 52,
),
child: Text(
'How about sending your first message to a friend?',
textAlign: TextAlign.center,
style: StreamChatTheme.of(context)
.textTheme
.body
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.grey,
),
),
),
],
),
),
if (widget.onStartChatPressed != null)
Positioned(
right: 0,
left: 0,
bottom: 32,
child: Center(
child: FlatButton(
onPressed: widget.onStartChatPressed,
child: Text(
'Start a chat',
style: StreamChatTheme.of(context)
.textTheme
.bodyBold
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.accentBlue,
),
),
),
),
),
],
),
);
},
);
}
if (channels.isNotEmpty) {
if (widget.crossAxisCount > 1) {
child = GridView.builder(
padding: widget.padding,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: widget.crossAxisCount),
itemCount: channels.length,
physics: AlwaysScrollableScrollPhysics(),
controller: _scrollController,
itemBuilder: (context, index) {
return _gridItemBuilder(context, index, channels);
},
);
} else {
child = ListView.separated(
padding: widget.padding,
physics: AlwaysScrollableScrollPhysics(),
itemCount:
channels.isNotEmpty ? channels.length + 1 : channels.length,
separatorBuilder: (_, index) {
if (widget.separatorBuilder != null) {
return widget.separatorBuilder(context, index);
}
return _separatorBuilder(context, index);
},
itemBuilder: (context, index) {
return _listItemBuilder(context, index, channels);
},
);
}
}
}
return AnimatedSwitcher(
child: child,
duration: Duration(milliseconds: 500),
);
},
);
}
Widget _buildLoadingWidget() {
return ListView(
padding: widget.padding,
physics: AlwaysScrollableScrollPhysics(),
children: List.generate(
25,
(i) {
if (widget.crossAxisCount == 1) {
if (i % 2 != 0) {
if (widget.separatorBuilder != null) {
return widget.separatorBuilder(context, i);
}
return _separatorBuilder(context, i);
}
}
return _buildLoadingItem();
},
),
);
}
Shimmer _buildLoadingItem() {
if (widget.crossAxisCount > 1) {
return Shimmer.fromColors(
baseColor: StreamChatTheme.of(context).colorTheme.greyGainsboro,
highlightColor: StreamChatTheme.of(context).colorTheme.whiteSmoke,
child: Column(
children: [
SizedBox(height: 4.0),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
for (int i = 0; i < widget.crossAxisCount; i++)
Container(
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
constraints: BoxConstraints.tightFor(
height: 70,
width: 70,
),
),
],
),
SizedBox(
height: 16.0,
),
],
),
);
} else {
return Shimmer.fromColors(
baseColor: StreamChatTheme.of(context).colorTheme.greyGainsboro,
highlightColor: StreamChatTheme.of(context).colorTheme.whiteSmoke,
child: ListTile(
leading: Container(
decoration: BoxDecoration(
color: StreamChatTheme.of(context).colorTheme.white,
shape: BoxShape.circle,
),
constraints: BoxConstraints.tightFor(
height: 40,
width: 40,
),
),
contentPadding: const EdgeInsets.only(
left: 8,
right: 8,
),
title: Align(
alignment: Alignment.centerLeft,
child: Container(
decoration: BoxDecoration(
color: StreamChatTheme.of(context).colorTheme.white,
borderRadius: BorderRadius.circular(11),
),
constraints: BoxConstraints.tightFor(
height: 16,
width: 82,
),
),
),
subtitle: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Align(
alignment: Alignment.centerLeft,
child: Container(
decoration: BoxDecoration(
color: StreamChatTheme.of(context).colorTheme.white,
borderRadius: BorderRadius.circular(11),
),
constraints: BoxConstraints.tightFor(
height: 16,
width: 238,
),
),
),
Container(
margin: const EdgeInsets.only(left: 16),
decoration: BoxDecoration(
color: StreamChatTheme.of(context).colorTheme.white,
borderRadius: BorderRadius.circular(11),
),
constraints: BoxConstraints.tightFor(
height: 16,
width: 42,
),
),
],
),
),
);
}
}
Widget _buildErrorWidget(
AsyncSnapshot<List<Channel>> snapshot,
BuildContext context,
ChannelsBlocState channelsBlocState,
) {
if (snapshot.error is Error) {
print((snapshot.error as Error).stackTrace);
}
if (widget.errorBuilder != null) {
return widget.errorBuilder(snapshot.error);
}
var message = snapshot.error.toString();
if (snapshot.error is DioError) {
final dioError = snapshot.error as DioError;
if (dioError.type == DioErrorType.RESPONSE) {
message = dioError.message;
} else {
message = 'Check your connection and retry';
}
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text.rich(
TextSpan(
children: [
WidgetSpan(
child: Padding(
padding: const EdgeInsets.only(
right: 2.0,
),
child: Icon(Icons.error_outline),
),
),
TextSpan(text: 'Error loading channels'),
],
),
style: Theme.of(context).textTheme.headline6,
),
Padding(
padding: const EdgeInsets.only(
top: 16.0,
),
child: Text(message),
),
FlatButton(
onPressed: () {
channelsBlocState.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination,
options: widget.options,
);
},
child: Text('Retry'),
),
],
),
);
}
Widget _listItemBuilder(BuildContext context, int i, List<Channel> channels) {
final channelsProvider = ChannelsBloc.of(context);
if (i < channels.length) {
final channel = channels[i];
ChannelTapCallback onTap;
if (widget.onChannelTap != null) {
onTap = widget.onChannelTap;
} else {
onTap = (client, _) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return StreamChannel(
child: widget.channelWidget,
channel: client,
);
},
),
);
};
}
return StreamChannel(
key: ValueKey<String>('CHANNEL-${channel.id}'),
channel: channel,
child: Builder(
builder: (context) {
Widget child;
if (widget.channelPreviewBuilder != null) {
child = Stack(
children: [
widget.channelPreviewBuilder(
context,
channel,
),
Positioned.fill(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
onTap(channel, widget.channelWidget);
},
onLongPress: widget.onChannelLongPress != null
? () {
widget.onChannelLongPress(channel);
}
: null,
),
),
),
],
);
} else {
final backgroundColor =
StreamChatTheme.of(context).colorTheme.whiteSmoke;
child = Slidable(
controller: _slideController,
enabled: widget.swipeToAction,
actionPane: SlidableBehindActionPane(),
actionExtentRatio: 0.12,
closeOnScroll: true,
secondaryActions: <Widget>[
IconSlideAction(
color: backgroundColor,
icon: Icons.more_horiz,
onTap: () {
showModalBottomSheet(
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(32),
topRight: Radius.circular(32),
),
),
context: context,
builder: (context) {
return StreamChannel(
child: ChannelBottomSheet(
onViewInfoTap: () {
widget.onViewInfoTap(channel);
},
),
channel: channel,
);
},
);
},
),
if ([
'admin',
'owner',
].contains(channel.state.members
.firstWhere(
(m) => m.userId == channel.client.state.user.id,
orElse: () => null)
?.role))
IconSlideAction(
color: backgroundColor,
iconWidget: StreamSvgIcon.delete(
color: StreamChatTheme.of(context).colorTheme.accentRed,
),
onTap: () async {
final res = await showConfirmationDialog(
context,
title: 'Delete Conversation',
okText: 'DELETE',
question:
'Are you sure you want to delete this conversation?',
cancelText: 'CANCEL',
icon: StreamSvgIcon.delete(
color: StreamChatTheme.of(context)
.colorTheme
.accentRed,
),
);
if (res == true) {
await channel.delete();
}
},
),
],
child: Container(
color: StreamChatTheme.of(context).colorTheme.whiteSnow,
child: ChannelPreview(
onLongPress: widget.onChannelLongPress,
channel: channel,
onImageTap: widget.onImageTap != null
? () {
widget.onImageTap(channel);
}
: null,
onTap: (channel) {
onTap(channel, widget.channelWidget);
},
),
),
);
}
return child;
},
),
);
} else {
return _buildQueryProgressIndicator(context, channelsProvider);
}
}
Widget _gridItemBuilder(BuildContext context, int i, List<Channel> channels) {
var channel = channels[i];
var selected = widget.selectedChannels.contains(channel);
return Container(
key: ValueKey<String>('CHANNEL-${channel.id}'),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ChannelImage(
channel: channel,
borderRadius: BorderRadius.circular(32),
selected: selected,
constraints: BoxConstraints.tightFor(
width: 64,
height: 64,
),
onTap: () {
widget.onChannelTap(channel, null);
},
),
SizedBox(height: 7),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: StreamChannel(
child: ChannelName(
textStyle: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
),
),
channel: channel,
),
),
],
),
);
}
Widget _buildQueryProgressIndicator(
context,
ChannelsBlocState channelsProvider,
) {
return StreamBuilder<bool>(
stream: channelsProvider.queryChannelsLoading,
initialData: false,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Container(
color: StreamChatTheme.of(context)
.colorTheme
.accentRed
.withOpacity(.2),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Center(
child: Text('Error loading channels'),
),
),
);
}
return Container(
height: 100,
padding: EdgeInsets.all(32),
child: Center(
child: snapshot.data ? CircularProgressIndicator() : Container(),
),
);
});
}
Widget _separatorBuilder(context, i) {
var effect = StreamChatTheme.of(context).colorTheme.borderTop;
return BackdropFilter(
filter: ui.ImageFilter.blur(
sigmaX: effect.sigmaX,
sigmaY: effect.sigmaY,
),
child: Container(
height: 1,
color: effect.color.withOpacity(0.08),
),
);
}
void _listenChannelPagination(ChannelsBlocState channelsProvider) {
if (_scrollController.position.maxScrollExtent ==
_scrollController.offset &&
_scrollController.offset != 0) {
channelsProvider.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination.copyWith(
offset: channelsProvider.channels?.length ?? 0,
),
options: widget.options,
);
}
}
StreamSubscription _subscription;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
final channelsBloc = ChannelsBloc.of(context);
channelsBloc.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination,
options: widget.options,
);
_scrollController.addListener(() {
channelsBloc.queryChannelsLoading.first.then((loading) {
if (!loading) {
_listenChannelPagination(channelsBloc);
}
});
});
final client = StreamChat.of(context).client;
_subscription = client
.on(
EventType.connectionRecovered,
EventType.notificationAddedToChannel,
EventType.notificationMessageNew,
EventType.channelVisible,
)
.listen((event) {
channelsBloc.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination,
options: widget.options,
);
});
}
@override
void didUpdateWidget(ChannelListView oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.filter?.toString() != oldWidget.filter?.toString() ||
jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) ||
widget.pagination?.toJson()?.toString() !=
oldWidget.pagination?.toJson()?.toString() ||
widget.options?.toString() != oldWidget.options?.toString()) {
final channelsBloc = ChannelsBloc.of(context);
channelsBloc.queryChannels(
filter: widget.filter,
sortOptions: widget.sort,
paginationParams: widget.pagination,
options: widget.options,
);
}
}
@override
void dispose() {
_subscription.cancel();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
}
-231
View File
@@ -1,231 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart';
class ChannelMediaDisplayScreen extends StatefulWidget {
/// The sorting used for the channels matching the filters.
/// Sorting is based on field and direction, multiple sorting options can be provided.
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
/// Direction can be ascending or descending.
final List<SortOption> sortOptions;
/// Pagination parameters
/// limit: the number of users to return (max is 30)
/// offset: the offset (max is 1000)
/// message_limit: how many messages should be included to each channel
final PaginationParams paginationParams;
/// The builder used when the file list is empty.
final WidgetBuilder emptyBuilder;
final ShowMessageCallback onShowMessage;
const ChannelMediaDisplayScreen({
this.sortOptions,
this.paginationParams,
this.emptyBuilder,
this.onShowMessage,
});
@override
_ChannelMediaDisplayScreenState createState() =>
_ChannelMediaDisplayScreenState();
}
class _ChannelMediaDisplayScreenState extends State<ChannelMediaDisplayScreen> {
@override
void initState() {
super.initState();
final messageSearchBloc = MessageSearchBloc.of(context);
messageSearchBloc.search(
filter: {
'cid': {
r'$in': ['messaging:${StreamChannel.of(context).channel.id}']
}
},
messageFilter: {
'attachments.type': {
r'$in': ['image', 'video']
},
},
sort: widget.sortOptions,
pagination: widget.paginationParams,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
appBar: AppBar(
brightness: Theme.of(context).brightness,
elevation: 1,
centerTitle: true,
title: Text(
'Photos & Videos',
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.black,
fontSize: 16.0,
),
),
leading: Center(
child: InkWell(
onTap: () {
Navigator.of(context).pop();
},
child: Container(
child: StreamSvgIcon.left(
color: StreamChatTheme.of(context).colorTheme.black,
size: 24.0,
),
width: 24.0,
height: 24.0,
),
),
),
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
),
body: _buildMediaGrid(),
);
}
Widget _buildMediaGrid() {
final messageSearchBloc = MessageSearchBloc.of(context);
return StreamBuilder<List<GetMessageResponse>>(
builder: (context, snapshot) {
if (snapshot.data == null) {
return Center(
child: const CircularProgressIndicator(),
);
}
if (snapshot.data.isEmpty) {
if (widget.emptyBuilder != null) {
return widget.emptyBuilder(context);
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
StreamSvgIcon.pictures(
size: 136.0,
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
),
SizedBox(height: 16.0),
Text(
'No Media',
style: TextStyle(
fontSize: 14.0,
color: StreamChatTheme.of(context).colorTheme.black,
),
),
SizedBox(height: 8.0),
Text(
'Photos or video sent in this chat will \nappear here',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.0,
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
),
),
],
),
);
}
final media = <_AssetPackage>[];
for (var item in snapshot.data) {
item.message.attachments
.where((e) => e.type == 'image' || e.type == 'video')
.forEach((e) {
VideoPlayerController controller;
if (e.type == 'video') {
controller = VideoPlayerController.network(e.assetUrl);
controller.initialize();
}
media.add(_AssetPackage(e, item.message, controller));
});
}
return LazyLoadScrollView(
onEndOfPage: () => messageSearchBloc.search(
filter: {
'cid': {
r'$in': ['messaging:${StreamChannel.of(context).channel.id}']
}
},
messageFilter: {
'attachments.type': {
r'$in': ['image', 'video']
},
},
sort: widget.sortOptions,
pagination: widget.paginationParams.copyWith(
offset: messageSearchBloc.messageResponses?.length ?? 0,
),
),
child: GridView.builder(
gridDelegate:
SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
itemBuilder: (context, position) {
var channel = StreamChannel.of(context).channel;
return Padding(
padding: const EdgeInsets.all(1.0),
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: FullScreenMedia(
mediaAttachments:
media.map((e) => e.attachment).toList(),
startIndex: position,
message: media[position].message,
sentAt: media[position].message.createdAt,
userName: media[position].message.user.name,
onShowMessage: widget.onShowMessage,
),
),
),
);
},
child: media[position].attachment.type == 'image'
? IgnorePointer(
child: ImageAttachment(
attachment: media[position].attachment,
message: media[position].message,
showTitle: false,
size: Size(
MediaQuery.of(context).size.width * 0.8,
MediaQuery.of(context).size.height * 0.3,
),
),
)
: VideoPlayer(media[position].videoPlayer),
),
);
},
itemCount: media.length,
),
);
},
stream: messageSearchBloc.messagesStream,
);
}
}
class _AssetPackage {
Attachment attachment;
Message message;
VideoPlayerController videoPlayer;
_AssetPackage(this.attachment, this.message, this.videoPlayer);
}
-78
View File
@@ -1,78 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:stream_chat/stream_chat.dart';
import '../stream_chat_flutter.dart';
import 'stream_channel.dart';
/// It shows the current [Channel] name using a [Text] widget.
///
/// The widget uses a [StreamBuilder] to render the channel information image as soon as it updates.
class ChannelName extends StatelessWidget {
/// Instantiate a new ChannelName
const ChannelName({
Key key,
this.textStyle,
}) : super(key: key);
/// The style of the text displayed
final TextStyle textStyle;
@override
Widget build(BuildContext context) {
final client = StreamChat.of(context);
final channel = StreamChannel.of(context).channel;
return StreamBuilder<Map<String, dynamic>>(
stream: channel.extraDataStream,
initialData: channel.extraData,
builder: (context, snapshot) {
return _buildName(snapshot.data, channel.state.members, client);
},
);
}
Widget _buildName(
Map<String, dynamic> extraData,
List<Member> members,
StreamChatState client,
) {
return LayoutBuilder(
builder: (context, constraints) {
String title;
if (extraData['name'] == null) {
final otherMembers =
members.where((member) => member.userId != client.user.id);
if (otherMembers.isNotEmpty) {
final maxWidth = constraints.maxWidth;
final maxChars = maxWidth / textStyle.fontSize;
var currentChars = 0;
final currentMembers = <Member>[];
otherMembers.forEach((element) {
final newLength = currentChars + element.user.name.length;
if (newLength < maxChars) {
currentChars = newLength;
currentMembers.add(element);
}
});
final exceedingMembers =
otherMembers.length - currentMembers.length;
title =
'${currentMembers.map((e) => e.user.name).join(', ')} ${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}';
} else {
title = 'No title';
}
} else {
title = extraData['name'];
}
return Text(
title,
style: textStyle,
overflow: TextOverflow.ellipsis,
);
},
);
}
}
-298
View File
@@ -1,298 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import '../stream_chat_flutter.dart';
import 'channel_name.dart';
import 'channel_unread_indicator.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview_paint.png)
///
/// It shows the current [Channel] preview.
///
/// The widget uses a [StreamBuilder] to render the channel information image as soon as it updates.
///
/// Usually you don't use this widget as it's the default channel preview used by [ChannelListView].
///
/// The widget renders the ui based on the first ancestor of type [StreamChatTheme].
/// Modify it to change the widget appearance.
class ChannelPreview extends StatelessWidget {
/// Function called when tapping this widget
final void Function(Channel) onTap;
/// Function called when long pressing this widget
final void Function(Channel) onLongPress;
/// Channel displayed
final Channel channel;
/// The function called when the image is tapped
final VoidCallback onImageTap;
ChannelPreview({
@required this.channel,
Key key,
this.onTap,
this.onLongPress,
this.onImageTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return StreamBuilder<bool>(
stream: channel.isMutedStream,
initialData: channel.isMuted,
builder: (context, snapshot) {
return Opacity(
opacity: snapshot.data ? 0.5 : 1,
child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 8,
),
onTap: () {
if (onTap != null) {
onTap(channel);
}
},
onLongPress: () {
if (onLongPress != null) {
onLongPress(channel);
}
},
leading: ChannelImage(
onTap: onImageTap,
),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(
child: ChannelName(
textStyle:
StreamChatTheme.of(context).channelPreviewTheme.title,
),
),
StreamBuilder<List<Member>>(
stream: channel.state.membersStream,
initialData: channel.state.members,
builder: (context, snapshot) {
if (!snapshot.hasData ||
snapshot.data.isEmpty ||
!snapshot.data.any((Member e) =>
e.user.id == channel.client.state.user.id)) {
return SizedBox();
}
return ChannelUnreadIndicator(
channel: channel,
);
}),
],
),
subtitle: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(child: _buildSubtitle(context)),
Builder(
builder: (context) {
if (channel.state.lastMessage?.user?.id ==
StreamChat.of(context).user.id) {
return Padding(
padding: const EdgeInsets.only(right: 4.0),
child: SendingIndicator(
message: channel.state.lastMessage,
size: StreamChatTheme.of(context)
.channelPreviewTheme
.indicatorIconSize,
isMessageRead: channel.state.read
?.where((element) =>
element.user.id !=
channel.client.state.user.id)
?.where((element) => element.lastRead
.isAfter(channel
.state.lastMessage.createdAt))
?.isNotEmpty ==
true,
),
);
}
return SizedBox();
},
),
_buildDate(context),
],
),
),
);
});
}
Widget _buildDate(BuildContext context) {
return StreamBuilder<DateTime>(
stream: channel.lastMessageAtStream,
initialData: channel.lastMessageAt,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return SizedBox();
}
final lastMessageAt = snapshot.data.toLocal();
String stringDate;
final now = DateTime.now();
var startOfDay = DateTime(now.year, now.month, now.day);
if (lastMessageAt.millisecondsSinceEpoch >=
startOfDay.millisecondsSinceEpoch) {
stringDate = Jiffy(lastMessageAt.toLocal()).format('HH:mm');
} else if (lastMessageAt.millisecondsSinceEpoch >=
startOfDay.subtract(Duration(days: 1)).millisecondsSinceEpoch) {
stringDate = 'Yesterday';
} else if (startOfDay.difference(lastMessageAt).inDays < 7) {
stringDate = Jiffy(lastMessageAt.toLocal()).EEEE;
} else {
stringDate = Jiffy(lastMessageAt.toLocal()).format('dd/MM/yyyy');
}
return Text(
stringDate,
style: StreamChatTheme.of(context).channelPreviewTheme.lastMessageAt,
);
},
);
}
Widget _buildSubtitle(BuildContext context) {
if (channel.isMuted) {
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
StreamSvgIcon.mute(
size: 16,
),
Text(
' Channel is muted',
style: StreamChatTheme.of(context)
.channelPreviewTheme
.subtitle
.copyWith(
color: StreamChatTheme.of(context)
.channelPreviewTheme
.subtitle
.color,
),
),
],
);
}
return TypingIndicator(
channel: channel,
alternativeWidget: _buildLastMessage(context),
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
color:
StreamChatTheme.of(context).channelPreviewTheme.subtitle.color,
),
);
}
Widget _buildLastMessage(BuildContext context) {
return StreamBuilder<List<Message>>(
stream: channel.state.messagesStream,
initialData: channel.state.messages,
builder: (context, snapshot) {
final lastMessage = snapshot.data
?.lastWhere((m) => m.shadowed != true, orElse: () => null);
if (lastMessage == null) {
return SizedBox();
}
var text = lastMessage.text;
if (lastMessage.isDeleted) {
text = 'This message was deleted.';
} else if (lastMessage.attachments != null) {
final parts = <String>[
...lastMessage.attachments.map((e) {
if (e.type == 'image') {
return '📷';
} else if (e.type == 'video') {
return '🎬';
} else if (e.type == 'giphy') {
return '[GIF]';
}
return e == lastMessage.attachments.last
? (e.title ?? 'File')
: '${e.title ?? 'File'} , ';
}).where((e) => e != null),
lastMessage.text ?? '',
];
text = parts.join(' ');
}
return Text.rich(
_getDisplayText(
text,
lastMessage.mentionedUsers,
lastMessage.attachments,
StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
color: StreamChatTheme.of(context)
.channelPreviewTheme
.subtitle
.color,
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted)
? FontStyle.italic
: FontStyle.normal),
StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
color: StreamChatTheme.of(context)
.channelPreviewTheme
.subtitle
.color,
fontStyle: (lastMessage.isSystem || lastMessage.isDeleted)
? FontStyle.italic
: FontStyle.normal,
fontWeight: FontWeight.bold),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
},
);
}
TextSpan _getDisplayText(
String text,
List<User> mentions,
List<Attachment> attachments,
TextStyle normalTextStyle,
TextStyle mentionsTextStyle) {
var textList = text.split(' ');
List<TextSpan> resList = [];
for (var e in textList) {
if (mentions != null &&
mentions.isNotEmpty &&
mentions.any((element) => '@${element.name}' == e)) {
resList.add(TextSpan(
text: '$e ',
style: mentionsTextStyle,
));
} else if (attachments != null &&
attachments.isNotEmpty &&
attachments
.where((e) => e.title != null)
.any((element) => element.title == e)) {
resList.add(TextSpan(
text: '$e ',
style: normalTextStyle.copyWith(fontStyle: FontStyle.italic),
));
} else {
resList.add(TextSpan(
text: e == textList.last ? '$e' : '$e ',
style: normalTextStyle,
));
}
}
return TextSpan(children: resList);
}
}
-49
View File
@@ -1,49 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
class ChannelUnreadIndicator extends StatelessWidget {
const ChannelUnreadIndicator({
Key key,
@required this.channel,
}) : super(key: key);
final Channel channel;
@override
Widget build(BuildContext context) {
return StreamBuilder<int>(
stream: channel.state.unreadCountStream,
initialData: channel.state.unreadCount,
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == 0) {
return SizedBox();
}
return Material(
borderRadius: BorderRadius.circular(8),
color: StreamChatTheme.of(context)
.channelPreviewTheme
.unreadCounterColor,
child: Padding(
padding: const EdgeInsets.only(
left: 5.0,
right: 5.0,
top: 2,
bottom: 1,
),
child: Center(
child: Text(
'${snapshot.data > 99 ? '99+' : snapshot.data}',
style: TextStyle(
fontSize: 11,
color: Colors.white,
),
),
),
),
);
},
);
}
}
-185
View File
@@ -1,185 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_chat.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Widget dedicated to the management of a channel list with pagination
class ChannelsBloc extends StatefulWidget {
/// The widget child
final Widget child;
/// Set this to true to prevent channels to be brought to the top of the list when a new message arrives
final bool lockChannelsOrder;
/// Comparator used to sort the channels when a message.new event is received
final Comparator<Channel> channelsComparator;
/// Function used to evaluate if a channel should be added to the list when a message.new event is received
final bool Function(Event) shouldAddChannel;
/// Instantiate a new ChannelsBloc
const ChannelsBloc({
Key key,
this.child,
this.lockChannelsOrder = false,
this.channelsComparator,
this.shouldAddChannel,
}) : super(key: key);
@override
ChannelsBlocState createState() => ChannelsBlocState();
/// Use this method to get the current [ChannelsBlocState] instance
static ChannelsBlocState of(BuildContext context) {
ChannelsBlocState streamChatState;
streamChatState = context.findAncestorStateOfType<ChannelsBlocState>();
if (streamChatState == null) {
throw Exception('You must have a ChannelsBloc widget as ancestor');
}
return streamChatState;
}
}
/// The current state of the [ChannelsBloc]
class ChannelsBlocState extends State<ChannelsBloc>
with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
return widget.child;
}
/// The current channel list
List<Channel> get channels => _channelsController.value;
/// The current channel list as a stream
Stream<List<Channel>> get channelsStream => _channelsController.stream;
final BehaviorSubject<bool> _queryChannelsLoadingController =
BehaviorSubject.seeded(false);
final BehaviorSubject<List<Channel>> _channelsController = BehaviorSubject();
/// The stream notifying the state of queryChannel call
Stream<bool> get queryChannelsLoading =>
_queryChannelsLoadingController.stream;
final List<Channel> _hiddenChannels = [];
/// Calls [client.queryChannels] updating [queryChannelsLoading] stream
Future<void> queryChannels({
Map<String, dynamic> filter,
List<SortOption> sortOptions,
PaginationParams paginationParams,
Map<String, dynamic> options,
bool onlyOffline = false,
}) async {
final client = StreamChat.of(context).client;
if (client.state?.user == null ||
_queryChannelsLoadingController.value == true) {
return;
}
_queryChannelsLoadingController.sink.add(true);
try {
final clear = paginationParams == null ||
paginationParams.offset == null ||
paginationParams.offset == 0;
final oldChannels = List<Channel>.from(channels ?? []);
final _channels = await client.queryChannels(
filter: filter,
sort: sortOptions,
options: options,
paginationParams: paginationParams,
onlyOffline: onlyOffline,
);
if (clear) {
_channelsController.add(_channels);
} else {
final l = oldChannels + _channels;
_channelsController.add(l);
}
_queryChannelsLoadingController.sink.add(false);
} catch (err, stackTrace) {
print(err);
print(stackTrace);
_queryChannelsLoadingController.addError(err, stackTrace);
}
}
final List<StreamSubscription> _subscriptions = [];
@override
void initState() {
super.initState();
final client = StreamChat.of(context).client;
if (!widget.lockChannelsOrder) {
_subscriptions.add(client.on(EventType.messageNew).listen((e) {
final newChannels = List<Channel>.from(channels ?? []);
final index = newChannels.indexWhere((c) => c.cid == e.cid);
if (index > -1) {
if (index > 0) {
final channel = newChannels.removeAt(index);
newChannels.insert(0, channel);
}
} else if (widget.shouldAddChannel != null &&
widget.shouldAddChannel(e)) {
final hiddenIndex = _hiddenChannels.indexWhere((c) => c.cid == e.cid);
if (hiddenIndex > -1) {
newChannels.insert(0, _hiddenChannels[hiddenIndex]);
_hiddenChannels.removeAt(hiddenIndex);
} else {
if (client.state?.channels != null &&
client.state?.channels[e.cid] != null) {
newChannels.insert(0, client.state.channels[e.cid]);
}
}
}
if (widget.channelsComparator != null) {
newChannels.sort(widget.channelsComparator);
}
_channelsController.add(newChannels);
}));
}
_subscriptions.add(client.on(EventType.channelHidden).listen((event) async {
final newChannels = List<Channel>.from(channels ?? []);
final channelIndex = newChannels.indexWhere((c) => c.cid == event.cid);
if (channelIndex > -1) {
final channel = newChannels.removeAt(channelIndex);
_hiddenChannels.add(channel);
_channelsController.add(newChannels);
}
}));
_subscriptions.add(client
.on(EventType.channelDeleted, EventType.notificationRemovedFromChannel)
.listen((e) {
final channel = e.channel;
_channelsController
.add(List.from(channels..removeWhere((c) => c.cid == channel.cid)));
}));
}
@override
void dispose() {
_channelsController.close();
_queryChannelsLoadingController.close();
_subscriptions.forEach((s) => s.cancel());
super.dispose();
}
@override
bool get wantKeepAlive => true;
}
-20
View File
@@ -1,20 +0,0 @@
import 'dart:async';
import 'package:synchronized/synchronized.dart';
import 'package:video_compress/video_compress.dart';
class ICompressVideoService {
static final ICompressVideoService instance = ICompressVideoService._();
final _lock = Lock();
ICompressVideoService._();
Future<MediaInfo> compressVideo(String path) async {
return _lock.synchronized(() {
return VideoCompress.compressVideo(
path,
);
});
}
}
ICompressVideoService get CompressVideoService =>
ICompressVideoService.instance;
-58
View File
@@ -1,58 +0,0 @@
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
/// It shows a date divider depending on the date difference
class DateDivider extends StatelessWidget {
final DateTime dateTime;
const DateDivider({
Key key,
@required this.dateTime,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final createdAt = Jiffy(dateTime);
final now = DateTime.now();
String dayInfo;
if (Jiffy(createdAt).isSame(now, Units.DAY)) {
dayInfo = 'TODAY';
} else if (Jiffy(createdAt)
.isSame(now.subtract(Duration(days: 1)), Units.DAY)) {
dayInfo = 'YESTERDAY';
} else if (Jiffy(createdAt).isAfter(
now.subtract(Duration(days: 7)),
Units.DAY,
)) {
dayInfo = createdAt.format('EEEE').toUpperCase();
} else if (Jiffy(createdAt).isAfter(
Jiffy(now).subtract(years: 1),
Units.DAY,
)) {
dayInfo = createdAt.format('MMMM d').toUpperCase();
} else {
dayInfo = createdAt.format('MMMM d').toUpperCase();
}
return Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5),
borderRadius: BorderRadius.circular(
8,
),
),
child: Text(
dayInfo,
style: StreamChatTheme.of(context).textTheme.footnoteBold.copyWith(
color: StreamChatTheme.of(context).colorTheme.white,
fontWeight: FontWeight.bold,
),
),
),
);
}
}
-77
View File
@@ -1,77 +0,0 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
class DeletedMessage extends StatelessWidget {
const DeletedMessage({
Key key,
@required this.messageTheme,
this.borderRadiusGeometry,
this.shape,
this.borderSide,
this.reverse = false,
}) : super(key: key);
/// The theme of the message
final MessageTheme messageTheme;
/// The border radius of the message text
final BorderRadiusGeometry borderRadiusGeometry;
/// The shape of the message text
final ShapeBorder shape;
/// The borderside of the message text
final BorderSide borderSide;
/// If true the widget will be mirrored
final bool reverse;
@override
Widget build(BuildContext context) {
return Transform(
transform: Matrix4.rotationY(reverse ? pi : 0),
alignment: Alignment.center,
child: Material(
color: messageTheme.messageBackgroundColor,
shape: shape ??
RoundedRectangleBorder(
borderRadius: borderRadiusGeometry ?? BorderRadius.zero,
side: borderSide ??
BorderSide(
color: Theme.of(context).brightness == Brightness.dark
? StreamChatTheme.of(context)
.colorTheme
.white
.withAlpha(24)
: StreamChatTheme.of(context)
.colorTheme
.black
.withAlpha(24),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16,
),
child: Transform(
transform: Matrix4.rotationY(reverse ? pi : 0),
alignment: Alignment.center,
child: Text(
'Message deleted',
style: messageTheme.messageText.copyWith(
fontStyle: FontStyle.italic,
color: (Theme.of(context).brightness == Brightness.dark
? StreamChatTheme.of(context).colorTheme.white
: StreamChatTheme.of(context).colorTheme.black)
.withOpacity(.5),
),
),
),
),
),
);
}
}
-14
View File
@@ -1,14 +0,0 @@
extension StringExtension on String {
String capitalize() {
return "${this[0].toUpperCase()}${this.substring(1)}";
}
}
/// List extension
extension IterableX<T> on Iterable<T> {
/// Insert any item<T> inBetween the list items
List<T> insertBetween(T item) => expand((e) sync* {
yield item;
yield e;
}).skip(1).toList(growable: false);
}
-200
View File
@@ -1,200 +0,0 @@
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/utils.dart';
import 'package:video_compress/video_compress.dart';
import 'package:video_player/video_player.dart';
import 'media_utils.dart';
enum FileAttachmentType { local, online }
class FileAttachment extends StatefulWidget {
final Attachment attachment;
final Size size;
final Widget trailing;
final FileAttachmentType attachmentType;
final PlatformFile file;
const FileAttachment({
Key key,
@required this.attachment,
this.size,
this.trailing,
this.attachmentType = FileAttachmentType.online,
this.file,
}) : super(key: key);
@override
_FileAttachmentState createState() => _FileAttachmentState();
}
class _FileAttachmentState extends State<FileAttachment> {
VideoPlayerController _controller;
Future<void> _initializeVideoPlayerFuture;
@override
void initState() {
super.initState();
if (MediaUtils.getMimeType(widget.attachment.title).type == 'video') {
if (widget.attachmentType == FileAttachmentType.online) {
_controller = VideoPlayerController.network(
widget.attachment.assetUrl,
);
} else {
_controller = VideoPlayerController.file(
File.fromRawPath(widget.file.bytes),
);
}
_initializeVideoPlayerFuture = _controller.initialize();
}
}
@override
Widget build(BuildContext context) {
return Material(
child: Container(
width: widget.size?.width ?? 100,
height: 56.0,
decoration: BoxDecoration(
color: StreamChatTheme.of(context).colorTheme.white,
borderRadius:
widget.trailing != null ? BorderRadius.circular(16.0) : null,
border: widget.trailing != null
? Border.fromBorderSide(BorderSide(
color: StreamChatTheme.of(context).colorTheme.greyWhisper))
: null,
),
child: Row(
children: [
Container(
child: _getFileTypeImage(),
height: 40.0,
width: 33.33,
margin: EdgeInsets.all(8.0),
),
SizedBox(
width: 6.0,
),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.attachment?.title ?? 'File',
style: StreamChatTheme.of(context).textTheme.bodyBold,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(
height: 3.0,
),
Text(
'${getSizeText(widget.attachment.extraData['file_size'])}',
style: StreamChatTheme.of(context).textTheme.body.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5)),
),
],
),
),
Column(
children: [
widget.trailing ??
IconButton(
icon: StreamSvgIcon.cloud_download(
color: StreamChatTheme.of(context).colorTheme.black,
),
onPressed: () {
launchURL(context, widget.attachment.assetUrl);
},
),
],
),
],
),
),
);
}
Widget _getFileTypeImage() {
if ((MediaUtils.getMimeType(widget.attachment.title).type == 'image')) {
switch (widget.attachmentType) {
case FileAttachmentType.local:
return Image.memory(
widget.file.bytes,
fit: BoxFit.cover,
);
break;
case FileAttachmentType.online:
return CachedNetworkImage(
imageUrl: widget.attachment.imageUrl ??
widget.attachment.assetUrl ??
widget.attachment.thumbUrl,
fit: BoxFit.cover,
progressIndicatorBuilder: (context, _, progress) {
return Center(
child: Container(
width: 20.0,
height: 20.0,
child: CircularProgressIndicator(
backgroundColor:
StreamChatTheme.of(context).colorTheme.accentBlue,
),
),
);
},
);
break;
}
}
if ((MediaUtils.getMimeType(widget.attachment.title).type == 'video')) {
switch (widget.attachmentType) {
case FileAttachmentType.local:
return FutureBuilder<File>(
future: VideoCompress.getFileThumbnail(widget.file.path),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Image.asset(
'images/placeholder.png',
package: 'stream_chat_flutter',
);
}
return Image.file(
snapshot.data,
fit: BoxFit.cover,
);
},
);
break;
case FileAttachmentType.online:
return FutureBuilder(
future: _initializeVideoPlayerFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
);
} else {
return Center(child: CircularProgressIndicator());
}
},
);
break;
}
}
return getFileTypeImage(widget.attachment.extraData['mime_type']);
}
}
-268
View File
@@ -1,268 +0,0 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:photo_view/photo_view.dart';
import 'package:stream_chat_flutter/src/image_footer.dart';
import 'package:stream_chat_flutter/src/image_header.dart';
import 'package:video_player/video_player.dart';
import 'stream_channel.dart';
import '../stream_chat_flutter.dart';
typedef ShowMessageCallback = void Function(Message message, Channel channel);
/// A full screen image widget
class FullScreenMedia extends StatefulWidget {
/// The url of the image
final List<Attachment> mediaAttachments;
final Message message;
final int startIndex;
final String userName;
final DateTime sentAt;
final ShowMessageCallback onShowMessage;
/// Instantiate a new FullScreenImage
const FullScreenMedia({
Key key,
@required this.mediaAttachments,
this.message,
this.startIndex = 0,
this.userName = '',
this.sentAt,
this.onShowMessage,
}) : super(key: key);
@override
_FullScreenMediaState createState() => _FullScreenMediaState();
}
class _FullScreenMediaState extends State<FullScreenMedia>
with SingleTickerProviderStateMixin {
bool _optionsShown = true;
AnimationController _controller;
PageController _pageController;
int _currentPage;
List<VideoPackage> videoPackages = [];
@override
void initState() {
super.initState();
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 300));
_pageController = PageController(initialPage: widget.startIndex);
_currentPage = widget.startIndex;
widget.mediaAttachments
.where((element) => element.type == 'video')
.toList()
.forEach((element) {
videoPackages.add(VideoPackage(context, element, () {
setState(() {});
}));
});
}
@override
Widget build(BuildContext context) {
var videoAttachments = widget.mediaAttachments
.where((element) => element.type == 'video')
.toList();
return Scaffold(
resizeToAvoidBottomInset: false,
body: Stack(
children: [
AnimatedBuilder(
animation: _controller,
builder: (context, snapshot) {
return PageView.builder(
controller: _pageController,
onPageChanged: (val) {
setState(() {
_currentPage = val;
});
},
itemBuilder: (context, position) {
if (widget.mediaAttachments[position].type == 'image' ||
widget.mediaAttachments[position].type == 'giphy') {
return PhotoView(
imageProvider: CachedNetworkImageProvider(
widget.mediaAttachments[position].imageUrl ??
widget.mediaAttachments[position].assetUrl ??
widget.mediaAttachments[position].thumbUrl),
maxScale: PhotoViewComputedScale.covered,
minScale: PhotoViewComputedScale.contained,
heroAttributes: PhotoViewHeroAttributes(
tag: widget.mediaAttachments,
),
backgroundDecoration: BoxDecoration(
color: ColorTween(
begin: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.color,
end: Colors.black)
.lerp(_controller.value),
),
onTapUp: (a, b, c) {
setState(() {
_optionsShown = !_optionsShown;
});
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
},
);
} else if (widget.mediaAttachments[position].type ==
'video') {
var controllerPackage = videoPackages[videoAttachments
.indexOf(widget.mediaAttachments[position])];
if (!controllerPackage.initialised) {
return Center(
child: CircularProgressIndicator(),
);
}
return InkWell(
onTap: () {
setState(() {
_optionsShown = !_optionsShown;
});
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
},
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 50.0,
),
child: Chewie(
controller: controllerPackage.chewieController,
),
),
);
}
return Container();
},
itemCount: widget.mediaAttachments.length,
);
}),
AnimatedOpacity(
opacity: _optionsShown ? 1.0 : 0.0,
duration: Duration(milliseconds: 300),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ImageHeader(
userName: widget.userName,
sentAt: widget.message.createdAt == null
? ''
: 'Sent ${getDay(widget.message.createdAt)} at ${Jiffy(widget.sentAt.toLocal()).format('HH:mm')}',
onBackPressed: () {
Navigator.of(context).pop();
},
message: widget.message,
urls: widget.mediaAttachments,
currentIndex: _currentPage,
onShowMessage: () {
widget.onShowMessage(
widget.message, StreamChannel.of(context).channel);
},
),
ImageFooter(
currentPage: _currentPage,
totalPages: widget.mediaAttachments.length,
mediaAttachments: widget.mediaAttachments,
message: widget.message,
videoPackages: videoPackages,
mediaSelectedCallBack: (val) {
setState(() {
_currentPage = val;
_pageController.animateToPage(val,
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut);
Navigator.pop(context);
});
},
),
],
),
),
],
),
);
}
String getDay(DateTime dateTime) {
var now = DateTime.now();
if (DateTime(dateTime.year, dateTime.month, dateTime.day) ==
DateTime(now.year, now.month, now.day)) {
return 'today';
} else if (DateTime(now.year, now.month, now.day)
.difference(dateTime)
.inHours <
24) {
return 'yesterday';
} else {
return 'on ${Jiffy(dateTime).format("MMM do")}';
}
}
@override
void dispose() {
videoPackages.forEach((element) {
element.dispose();
});
super.dispose();
}
}
class VideoPackage {
VideoPlayerController _videoPlayerController;
ChewieController _chewieController;
bool initialised = false;
VoidCallback onInit;
BuildContext context;
///
VideoPackage(this.context, Attachment attachment, this.onInit) {
_videoPlayerController = VideoPlayerController.network(attachment.assetUrl);
_videoPlayerController.initialize().whenComplete(() {
initialised = true;
_chewieController = ChewieController(
videoPlayerController: _videoPlayerController,
autoInitialize: false,
aspectRatio: _videoPlayerController.value.aspectRatio,
);
onInit();
});
VoidCallback errorListener;
errorListener = () {
if (_videoPlayerController.value.hasError) {
Navigator.pop(context);
launchURL(context, attachment.titleLink);
}
_videoPlayerController.removeListener(errorListener);
};
_videoPlayerController.addListener(errorListener);
}
get videoPlayer => _videoPlayerController;
get chewieController => _chewieController;
void dispose() {
_videoPlayerController.dispose();
_chewieController.dispose();
}
}
-401
View File
@@ -1,401 +0,0 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import '../stream_chat_flutter.dart';
import 'attachment_error.dart';
import 'full_screen_media.dart';
class GiphyAttachment extends StatelessWidget {
final Attachment attachment;
final MessageTheme messageTheme;
final Message message;
final Size size;
final ShowMessageCallback onShowMessage;
const GiphyAttachment({
Key key,
this.attachment,
this.messageTheme,
this.message,
this.size,
this.onShowMessage,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (attachment.thumbUrl == null &&
attachment.imageUrl == null &&
attachment.assetUrl == null) {
return AttachmentError(
attachment: attachment,
);
}
return attachment.actions != null
? _buildSendingAttachment(context)
: _buildSentAttachment(context);
}
Widget _buildSendingAttachment(context) {
final streamChannel = StreamChannel.of(context);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Card(
elevation: 2,
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topRight: Radius.circular(16.0),
bottomRight: Radius.circular(0.0),
topLeft: Radius.circular(16.0),
bottomLeft: Radius.circular(16.0),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Stack(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: GestureDetector(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) {
final channel = StreamChannel.of(context).channel;
return StreamChannel(
channel: channel,
child: FullScreenMedia(
mediaAttachments: [
attachment,
],
userName: message.user.name,
sentAt: message.createdAt,
message: message,
onShowMessage: onShowMessage,
),
);
}));
},
child: ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
child: CachedNetworkImage(
height: size?.height,
width: size?.width,
placeholder: (_, __) {
return Container(
width: size?.width,
height: size?.height,
child: Center(
child: CircularProgressIndicator(),
),
);
},
imageUrl: attachment.thumbUrl ??
attachment.imageUrl ??
attachment.assetUrl,
errorWidget: (context, url, error) => AttachmentError(
attachment: attachment,
size: size,
),
fit: BoxFit.cover,
),
),
),
),
Positioned(
bottom: 16,
left: 16,
child: Material(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 4.0,
),
child: Row(
children: [
StreamSvgIcon.lightning(
color:
StreamChatTheme.of(context).colorTheme.white,
size: 16,
),
Text(
'GIPHY',
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.white,
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
],
),
),
),
),
],
),
if (attachment.title != null)
Container(
alignment: Alignment.bottomCenter,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Card(
elevation: 2,
child: IconButton(
padding: const EdgeInsets.all(0),
constraints: BoxConstraints.tight(Size(32, 32)),
icon: StreamSvgIcon.left(
size: 24.0,
),
splashRadius: 16,
onPressed: () {
streamChannel.channel.sendAction(message, {
'image_action': 'shuffle',
});
},
),
shape: CircleBorder(),
),
Expanded(
child: Center(
child: Text(
'"${attachment.title}"',
style: TextStyle(
fontStyle: FontStyle.italic,
),
),
),
),
Card(
elevation: 2,
child: IconButton(
padding: const EdgeInsets.all(0),
constraints: BoxConstraints.tight(Size(32, 32)),
icon: StreamSvgIcon.right(
size: 24.0,
),
splashRadius: 16,
onPressed: () {
streamChannel.channel.sendAction(message, {
'image_action': 'shuffle',
});
},
),
shape: CircleBorder(),
),
],
),
),
),
SizedBox(
height: 4.0,
),
Container(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.2),
width: double.infinity,
height: 0.5,
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: FlatButton(
height: 50,
onPressed: () {
streamChannel.channel.sendAction(message, {
'image_action': 'cancel',
});
},
child: Text(
'Cancel',
style: StreamChatTheme.of(context)
.textTheme
.bodyBold
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
),
),
),
),
Container(
width: 0.5,
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.2),
height: 50.0,
),
Expanded(
child: FlatButton(
height: 50,
onPressed: () {
streamChannel.channel.sendAction(message, {
'image_action': 'send',
});
},
child: Text(
'Send',
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.accentBlue,
fontWeight: FontWeight.bold),
),
),
),
],
),
],
),
),
SizedBox(
height: 4.0,
),
Align(
alignment: Alignment.centerRight,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
StreamSvgIcon.eye(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
size: 16.0,
),
SizedBox(
width: 8.0,
),
Text(
'Only visible to you',
style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5)),
),
],
),
),
),
],
);
}
Widget _buildSentAttachment(context) {
return Container(
child: GestureDetector(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) {
var channel = StreamChannel.of(context).channel;
return StreamChannel(
channel: channel,
child: FullScreenMedia(
mediaAttachments: [
attachment,
],
userName: message.user.name,
sentAt: message.createdAt,
message: message,
onShowMessage: onShowMessage,
),
);
}));
},
child: Stack(
children: [
CachedNetworkImage(
height: size?.height,
width: size?.width,
placeholder: (_, __) {
return Container(
width: size?.width,
height: size?.height,
child: Center(
child: CircularProgressIndicator(),
),
);
},
imageUrl: attachment.thumbUrl ??
attachment.imageUrl ??
attachment.assetUrl,
errorWidget: (context, url, error) => AttachmentError(
attachment: attachment,
size: size,
),
fit: BoxFit.cover,
),
Positioned(
bottom: 8,
left: 8,
child: Material(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 4.0,
),
child: Row(
children: [
StreamSvgIcon.lightning(
color: StreamChatTheme.of(context).colorTheme.white,
size: 16,
),
Text(
'GIPHY',
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.white,
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
],
),
),
),
),
],
),
),
);
}
}
-127
View File
@@ -1,127 +0,0 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import '../stream_chat_flutter.dart';
class GroupImage extends StatelessWidget {
const GroupImage({
Key key,
@required this.images,
this.constraints,
this.onTap,
this.borderRadius,
this.selected = false,
this.selectionColor,
this.selectionThickness = 4,
}) : super(key: key);
final List<String> images;
final BoxConstraints constraints;
final VoidCallback onTap;
final bool selected;
final BorderRadius borderRadius;
final Color selectionColor;
final double selectionThickness;
@override
Widget build(BuildContext context) {
var avatar;
final streamChatTheme = StreamChatTheme.of(context);
avatar = GestureDetector(
onTap: onTap,
child: ClipRRect(
borderRadius: borderRadius ??
StreamChatTheme.of(context)
.ownMessageTheme
.avatarTheme
.borderRadius,
child: Container(
constraints: constraints ??
StreamChatTheme.of(context)
.ownMessageTheme
.avatarTheme
.constraints,
decoration: BoxDecoration(
color: StreamChatTheme.of(context).colorTheme.accentBlue,
),
child: Flex(
direction: Axis.vertical,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Flexible(
fit: FlexFit.tight,
child: Flex(
direction: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: images
.take(2)
.map((url) => Flexible(
fit: FlexFit.tight,
child: FittedBox(
fit: BoxFit.cover,
clipBehavior: Clip.antiAlias,
child: Transform.scale(
scale: 1.2,
child: CachedNetworkImage(
imageUrl: url,
fit: BoxFit.cover,
),
),
),
))
.toList(),
),
),
if (images.length > 2)
Flexible(
fit: FlexFit.tight,
child: Flex(
direction: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: images
.skip(2)
.map((url) => Flexible(
fit: FlexFit.tight,
child: FittedBox(
fit: BoxFit.cover,
clipBehavior: Clip.antiAlias,
child: Transform.scale(
scale: 1.2,
child: CachedNetworkImage(
imageUrl: url,
fit: BoxFit.cover,
),
),
),
))
.toList(),
),
),
],
),
),
),
);
if (selected) {
avatar = ClipRRect(
borderRadius: (borderRadius ??
streamChatTheme.ownMessageTheme.avatarTheme.borderRadius) +
BorderRadius.circular(selectionThickness),
child: Container(
color: selectionColor ??
StreamChatTheme.of(context).colorTheme.accentBlue,
height: 64.0,
width: 64.0,
child: Padding(
padding: EdgeInsets.all(selectionThickness),
child: avatar,
),
),
);
}
return avatar;
}
}
-182
View File
@@ -1,182 +0,0 @@
import 'dart:typed_data';
import 'dart:ui';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:path_provider/path_provider.dart';
import '../stream_chat_flutter.dart';
class ImageActionsModal extends StatelessWidget {
final Message message;
final String userName;
final String sentAt;
final List<Attachment> urls;
final currentIndex;
final VoidCallback onShowMessage;
ImageActionsModal(
{this.message,
this.userName,
this.sentAt,
this.urls,
this.currentIndex,
this.onShowMessage});
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => Navigator.maybePop(context),
child: _buildPage(context),
);
}
Widget _buildPage(context) {
return Material(
color: Colors.transparent,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
SizedBox(
height: kToolbarHeight,
child: IconButton(
icon: StreamSvgIcon.close(),
onPressed: () => Navigator.maybePop(context),
),
),
Align(
alignment: Alignment.centerRight,
child: Container(
width: MediaQuery.of(context).size.width / 1.8,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Material(
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: ListTile.divideTiles(
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
context: context,
tiles: [
_buildButton(
context,
'Reply',
StreamSvgIcon.Icon_curve_line_left_up(
size: 24.0,
color:
StreamChatTheme.of(context).colorTheme.grey,
),
() {}),
_buildButton(
context,
'Show in Chat',
StreamSvgIcon.eye(
size: 24.0,
color:
StreamChatTheme.of(context).colorTheme.black,
),
onShowMessage),
_buildButton(
context,
'Save ${urls[currentIndex].type == 'video' ? 'Video' : 'Image'}',
StreamSvgIcon.Icon_save(
size: 24.0,
color:
StreamChatTheme.of(context).colorTheme.grey,
), () async {
var url = urls[currentIndex].imageUrl ??
urls[currentIndex].assetUrl ??
urls[currentIndex].thumbUrl;
if (urls[currentIndex].type == 'video') {
await _saveVideo(url);
Navigator.pop(context);
} else {
await _saveImage(url);
Navigator.pop(context);
}
}),
if (StreamChat.of(context).user.id == message.user.id)
_buildButton(
context,
'Delete',
StreamSvgIcon.delete(
size: 24.0,
color: StreamChatTheme.of(context)
.colorTheme
.accentRed,
),
() {
Navigator.pop(context);
Navigator.pop(context);
StreamChat.of(context).client.deleteMessage(
message,
StreamChannel.of(context).channel.cid,
);
},
color: StreamChatTheme.of(context)
.colorTheme
.accentRed,
),
],
).toList(),
),
),
),
),
),
],
),
);
}
Widget _buildButton(
context, String title, StreamSvgIcon icon, VoidCallback onTap,
{Color color}) {
var titleStyle = TextStyle(
fontSize: 14.5,
color: StreamChatTheme.of(context).colorTheme.black,
);
return Material(
color: StreamChatTheme.of(context).colorTheme.white,
child: InkWell(
onTap: onTap,
child: ListTile(
dense: true,
title: Text(
title,
style:
color == null ? titleStyle : titleStyle.copyWith(color: color),
),
leading: icon,
),
),
);
}
Future<void> _saveImage(String url) async {
var response = await Dio()
.get(url, options: Options(responseType: ResponseType.bytes));
final result = await ImageGallerySaver.saveImage(
Uint8List.fromList(response.data),
quality: 60,
name: "${DateTime.now().millisecondsSinceEpoch}");
return result;
}
Future<void> _saveVideo(String url) async {
var appDocDir = await getTemporaryDirectory();
var savePath =
appDocDir.path + "/${DateTime.now().millisecondsSinceEpoch}.mp4";
await Dio().download(url, savePath);
final result = await ImageGallerySaver.saveFile(savePath);
print(result);
}
}
-118
View File
@@ -1,118 +0,0 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import '../stream_chat_flutter.dart';
import 'attachment_error.dart';
import 'attachment_title.dart';
import 'full_screen_media.dart';
import 'utils.dart';
class ImageAttachment extends StatelessWidget {
final Attachment attachment;
final Message message;
final MessageTheme messageTheme;
final Size size;
final bool showTitle;
final ShowMessageCallback onShowMessage;
const ImageAttachment({
Key key,
@required this.attachment,
@required this.message,
@required this.size,
this.messageTheme,
this.showTitle = true,
this.onShowMessage,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (attachment.thumbUrl == null &&
attachment.imageUrl == null &&
attachment.assetUrl == null) {
return AttachmentError(
attachment: attachment,
);
}
return ConstrainedBox(
constraints: BoxConstraints.loose(size),
child: Stack(
children: <Widget>[
Column(
children: <Widget>[
Expanded(
child: GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) {
final channel = StreamChannel.of(context).channel;
return StreamChannel(
channel: channel,
child: FullScreenMedia(
mediaAttachments: [
attachment,
],
userName: message.user.name,
sentAt: message.createdAt,
message: message,
onShowMessage: onShowMessage,
),
);
},
),
);
},
child: CachedNetworkImage(
height: size?.height,
width: size?.width,
placeholder: (_, __) {
return Container(
width: size?.width,
height: size?.height,
child: Center(
child: CircularProgressIndicator(),
),
);
},
imageUrl: attachment.thumbUrl ??
attachment.imageUrl ??
attachment.assetUrl,
errorWidget: (context, url, error) => AttachmentError(
attachment: attachment,
size: size,
),
fit: BoxFit.cover,
),
),
),
if (showTitle && attachment.title != null)
Material(
color: messageTheme.messageBackgroundColor,
child: AttachmentTitle(
messageTheme: messageTheme,
attachment: attachment,
),
),
],
),
if (showTitle &&
(attachment.titleLink != null || attachment.ogScrapeUrl != null))
Positioned.fill(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => launchURL(
context,
attachment.titleLink ?? attachment.ogScrapeUrl,
),
),
),
),
],
),
);
}
}
-423
View File
@@ -1,423 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:chewie/chewie.dart';
import 'package:dio/dio.dart';
import 'package:esys_flutter_share/esys_flutter_share.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:path_provider/path_provider.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class ImageFooter extends StatefulWidget implements PreferredSizeWidget {
/// Callback to call when pressing the back button.
/// By default it calls [Navigator.pop]
final VoidCallback onBackPressed;
/// Callback to call when the header is tapped.
final VoidCallback onTitleTap;
/// Callback to call when the image is tapped.
final VoidCallback onImageTap;
final int currentPage;
final int totalPages;
final List<Attachment> mediaAttachments;
final Message message;
final List<VideoPackage> videoPackages;
final ValueChanged<int> mediaSelectedCallBack;
/// Creates a channel header
ImageFooter({
Key key,
this.onBackPressed,
this.onTitleTap,
this.onImageTap,
this.currentPage = 0,
this.totalPages = 0,
this.mediaAttachments,
this.message,
this.videoPackages,
this.mediaSelectedCallBack,
}) : preferredSize = Size.fromHeight(kToolbarHeight),
super(key: key);
@override
_ImageFooterState createState() => _ImageFooterState();
@override
final Size preferredSize;
}
class _ImageFooterState extends State<ImageFooter> {
bool _userSearchMode = false;
TextEditingController _searchController;
final TextEditingController _messageController = TextEditingController();
final FocusNode _messageFocusNode = FocusNode();
String _channelNameQuery;
final List<Channel> _selectedChannels = [];
bool _loading = false;
Timer _debounce;
Function modalSetStateCallback;
void _userNameListener() {
if (_debounce?.isActive ?? false) _debounce.cancel();
_debounce = Timer(const Duration(milliseconds: 350), () {
if (mounted && modalSetStateCallback != null) {
modalSetStateCallback(() {
_channelNameQuery = _searchController.text;
});
}
});
}
@override
void initState() {
super.initState();
_searchController = TextEditingController()..addListener(_userNameListener);
_messageFocusNode.addListener(() {
setState(() {});
});
}
@override
void dispose() {
_searchController?.clear();
_searchController?.removeListener(_userNameListener);
_searchController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox.fromSize(
size: Size(
MediaQuery.of(context).size.width,
MediaQuery.of(context).padding.bottom + widget.preferredSize.height,
),
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: BottomAppBar(
color: StreamChatTheme.of(context).colorTheme.white,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: StreamSvgIcon.icon_SHARE(
size: 24.0,
color: StreamChatTheme.of(context).colorTheme.black,
),
onPressed: () async {
final attachment =
widget.mediaAttachments[widget.currentPage];
var url = attachment.imageUrl ??
attachment.assetUrl ??
attachment.thumbUrl;
var type = attachment.type == 'image'
? 'jpg'
: url?.split('?')?.first?.split('.')?.last ?? 'jpg';
var request = await HttpClient().getUrl(Uri.parse(url));
var response = await request.close();
var bytes =
await consolidateHttpClientResponseBytes(response);
await Share.file('File', 'image.$type', bytes, 'image/$type');
},
),
InkWell(
onTap: widget.onTitleTap,
child: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
'${widget.currentPage + 1} of ${widget.totalPages}',
style:
StreamChatTheme.of(context).textTheme.headlineBold,
),
],
),
),
),
IconButton(
icon: StreamSvgIcon.Icon_grid(
color: StreamChatTheme.of(context).colorTheme.black,
),
onPressed: () => _showPhotosModal(context),
),
],
),
),
),
);
}
void _showPhotosModal(context) {
var videoAttachments = widget.mediaAttachments
.where((element) => element.type == 'video')
.toList();
showModalBottomSheet(
context: context,
barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0),
),
builder: (context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
decoration: BoxDecoration(
color: StreamChatTheme.of(context).colorTheme.white,
borderRadius: BorderRadius.only(
topRight: Radius.circular(16.0),
topLeft: Radius.circular(16.0),
)),
child: Stack(
children: [
Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Photos',
style:
StreamChatTheme.of(context).textTheme.headlineBold,
),
),
),
Align(
alignment: Alignment.centerRight,
child: IconButton(
icon: StreamSvgIcon.close(
color: StreamChatTheme.of(context).colorTheme.black,
),
onPressed: () => Navigator.maybePop(context),
),
),
],
),
),
Container(
color: StreamChatTheme.of(context).colorTheme.white,
child: GridView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, position) {
if (widget.mediaAttachments[position].type == 'video') {
var controllerPackage = widget.videoPackages[
videoAttachments
.indexOf(widget.mediaAttachments[position])];
return InkWell(
onTap: () {
widget.mediaSelectedCallBack(position);
},
child: FittedBox(
fit: BoxFit.cover,
child: Chewie(
controller: controllerPackage.chewieController,
),
),
);
} else {
return InkWell(
onTap: () {
widget.mediaSelectedCallBack(position);
},
child: Padding(
padding: const EdgeInsets.all(1.0),
child: AspectRatio(
child: CachedNetworkImage(
imageUrl: widget
.mediaAttachments[position].imageUrl ??
widget.mediaAttachments[position].assetUrl ??
widget.mediaAttachments[position].thumbUrl,
fit: BoxFit.cover,
),
aspectRatio: 1.0,
),
),
);
}
},
itemCount: widget.mediaAttachments.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3),
),
),
],
);
},
);
}
Widget _buildShareTextInputSection(modalSetState) {
return Align(
alignment: Alignment.bottomCenter,
child: BottomAppBar(
child: Container(
height: 40.0,
margin: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 8,
),
child: _loading
? Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: CircularProgressIndicator(),
),
)
: Row(
children: [
Expanded(
child: TextField(
controller: _messageController,
focusNode: _messageFocusNode,
onChanged: (val) {
modalSetState(() {});
},
onTap: () {
modalSetState(() {});
setState(() {});
},
decoration: InputDecoration(
prefixText: ' ',
hintText: 'Add a comment',
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(24.0),
borderSide: BorderSide(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.16),
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(24.0),
borderSide: BorderSide(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.16),
)),
contentPadding: const EdgeInsets.all(0),
),
),
),
SizedBox(width: 8),
IconTheme(
data: StreamChatTheme.of(context)
.channelTheme
.messageInputButtonIconTheme,
child: IconButton(
onPressed: () async {
modalSetState(() => _loading = true);
await sendMessage();
modalSetState(() => _loading = false);
},
splashRadius: 24,
visualDensity: VisualDensity.compact,
constraints: BoxConstraints.tightFor(
height: 24,
width: 24,
),
padding: EdgeInsets.zero,
icon: Transform.rotate(
angle: -pi / 2,
child: StreamSvgIcon.Icon_send_message(
color: StreamChatTheme.of(context)
.colorTheme
.accentBlue,
),
),
),
),
],
),
),
),
);
}
/// Sends the current message
Future sendMessage() async {
var text = _messageController.text.trim();
final attachments = widget.message.attachments;
_messageController.clear();
for (var channel in _selectedChannels) {
final message = Message(
text: text,
attachments: [attachments[widget.currentPage]],
);
await channel.sendMessage(message);
}
_selectedChannels.clear();
Navigator.pop(context);
}
Future<void> _saveImage(String url) async {
var response = await Dio()
.get(url, options: Options(responseType: ResponseType.bytes));
final result = await ImageGallerySaver.saveImage(
Uint8List.fromList(response.data),
quality: 60,
name: "${DateTime.now().millisecondsSinceEpoch}");
return result;
}
Future<void> _saveVideo(String url) async {
var appDocDir = await getTemporaryDirectory();
var savePath =
appDocDir.path + "/${DateTime.now().millisecondsSinceEpoch}.mp4";
await Dio().download(url, savePath);
final result = await ImageGallerySaver.saveFile(savePath);
print(result);
}
}
/// Used for clipping textfield prefix icon
class IconClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
var leftX = size.width / 5;
var rightX = 4 * size.width / 5;
var topY = size.height / 5;
var bottomY = 4 * size.height / 5;
final path = Path();
path.moveTo(leftX, topY);
path.lineTo(leftX, bottomY);
path.lineTo(rightX, bottomY);
path.lineTo(rightX, topY);
path.lineTo(leftX, topY);
path.lineTo(0.0, 0.0);
path.close();
return path;
}
@override
bool shouldReclip(CustomClipper oldClipper) {
return false;
}
}
-142
View File
@@ -1,142 +0,0 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/full_screen_media.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class ImageGroup extends StatelessWidget {
const ImageGroup({
Key key,
@required this.images,
@required this.message,
@required this.size,
this.onShowMessage,
}) : super(key: key);
final List<Attachment> images;
final Message message;
final Size size;
final ShowMessageCallback onShowMessage;
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: BoxConstraints.loose(size),
child: Flex(
direction: Axis.vertical,
children: <Widget>[
Flexible(
flex: 1,
fit: FlexFit.tight,
child: Flex(
crossAxisAlignment: CrossAxisAlignment.stretch,
direction: Axis.horizontal,
children: [
Flexible(
flex: 1,
fit: FlexFit.tight,
child: _buildImage(context, 0),
),
Flexible(
flex: 1,
fit: FlexFit.tight,
child: Padding(
padding: const EdgeInsets.only(left: 2.0),
child: _buildImage(context, 1),
),
),
],
),
),
if (images.length >= 3)
Flexible(
fit: FlexFit.tight,
flex: 1,
child: Padding(
padding: const EdgeInsets.only(top: 2.0),
child: Flex(
direction: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Flexible(
fit: FlexFit.tight,
flex: 1,
child: _buildImage(context, 2),
),
if (images.length >= 4)
Flexible(
fit: FlexFit.tight,
flex: 1,
child: Padding(
padding: const EdgeInsets.only(left: 2.0),
child: Stack(
fit: StackFit.expand,
children: <Widget>[
_buildImage(context, 3),
if (images.length > 4)
Positioned.fill(
child: GestureDetector(
onTap: () => _onTap(context, 3),
child: Material(
color: Colors.black38,
child: Center(
child: Text(
'+ ${images.length - 4}',
style: TextStyle(
color: Colors.white,
fontSize: 26,
),
),
),
),
),
),
],
),
),
),
],
),
),
),
],
),
);
}
void _onTap(
BuildContext context, [
int index,
]) {
final channel = StreamChannel.of(context).channel;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: FullScreenMedia(
mediaAttachments: images,
startIndex: index,
userName: message.user.name,
sentAt: message.createdAt,
message: message,
onShowMessage: onShowMessage,
),
),
),
);
}
Widget _buildImage(BuildContext context, int index) {
return GestureDetector(
onTap: () => _onTap(context, index),
child: CachedNetworkImage(
imageUrl: images[index].imageUrl ??
images[index].thumbUrl ??
images[index].assetUrl,
fit: BoxFit.cover,
),
);
}
}
-124
View File
@@ -1,124 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'image_actions_modal.dart';
import 'stream_channel.dart';
class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
/// True if this header shows the leading back button
final bool showBackButton;
/// Callback to call when pressing the back button.
/// By default it calls [Navigator.pop]
final VoidCallback onBackPressed;
/// Callback to call when pressing the show message button.
final VoidCallback onShowMessage;
/// Callback to call when the header is tapped.
final VoidCallback onTitleTap;
/// Callback to call when the image is tapped.
final VoidCallback onImageTap;
final Message message;
final String userName;
final String sentAt;
final List<Attachment> urls;
final currentIndex;
/// Creates a channel header
ImageHeader({
Key key,
this.message,
this.urls,
this.currentIndex,
this.showBackButton = true,
this.onBackPressed,
this.onShowMessage,
this.onTitleTap,
this.onImageTap,
this.userName = '',
this.sentAt = '',
}) : preferredSize = Size.fromHeight(kToolbarHeight),
super(key: key);
@override
Widget build(BuildContext context) {
return AppBar(
brightness: Theme.of(context).brightness,
elevation: 1,
leading: showBackButton
? IconButton(
icon: StreamSvgIcon.close(
color: StreamChatTheme.of(context).colorTheme.black,
size: 24.0,
),
onPressed: onBackPressed,
)
: SizedBox(),
backgroundColor:
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
actions: <Widget>[
IconButton(
icon: StreamSvgIcon.Icon_menu_point_v(
color: StreamChatTheme.of(context).colorTheme.black,
),
onPressed: () {
_showMessageActionModalBottomSheet(context);
},
),
],
centerTitle: true,
title: InkWell(
onTap: onTitleTap,
child: Container(
height: preferredSize.height,
width: preferredSize.width,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
userName,
style: StreamChatTheme.of(context).textTheme.headlineBold,
),
Text(
sentAt,
style: StreamChatTheme.of(context).channelPreviewTheme.subtitle,
),
],
),
),
),
);
}
@override
final Size preferredSize;
void _showMessageActionModalBottomSheet(BuildContext context) {
final channel = StreamChannel.of(context).channel;
showDialog(
context: context,
barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
builder: (context) {
return StreamChannel(
channel: channel,
child: ImageActionsModal(
userName: userName,
sentAt: sentAt,
message: message,
urls: urls,
currentIndex: currentIndex,
onShowMessage: onShowMessage,
),
);
});
}
}
-148
View File
@@ -1,148 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
enum _LoadingStatus { LOADING, STABLE }
/// A widget that wraps a [Widget] and will trigger [onEndOfPage]/[onStartOfPage] when it
/// reaches the bottom/start of the list
class LazyLoadScrollView extends StatefulWidget {
/// The [Widget] that this widget watches for changes on
final Widget child;
/// Called when the [child] reaches the start of the list
final AsyncCallback onStartOfPage;
/// Called when the [child] reaches the end of the list
final AsyncCallback onEndOfPage;
/// Called when the list scrolling starts
final VoidCallback onPageScrollStart;
/// Called when the list scrolling ends
final VoidCallback onPageScrollEnd;
/// Called every time the [child] is in-between the list
final VoidCallback onInBetweenOfPage;
/// The offset to take into account when triggering [onEndOfPage]/[onStartOfPage] in pixels
final double scrollOffset;
/// Used to determine if loading of new data has finished. You should use set this if you aren't using a FutureBuilder or StreamBuilder
final bool isLoading;
/// Initiates a LazyLoadScrollView widget
const LazyLoadScrollView({
Key key,
@required this.child,
this.onStartOfPage,
this.onEndOfPage,
this.onPageScrollStart,
this.onPageScrollEnd,
this.onInBetweenOfPage,
this.isLoading = false,
this.scrollOffset = 100,
}) : assert(child != null),
super(key: key);
@override
State<StatefulWidget> createState() => _LazyLoadScrollViewState();
}
class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
_LoadingStatus _loadMoreStatus = _LoadingStatus.STABLE;
double _scrollPosition = 0.0;
@override
Widget build(BuildContext context) {
return NotificationListener(
child: widget.child,
onNotification: _onNotification,
);
}
bool _onNotification(Notification notification) {
if (notification is ScrollStartNotification) {
if (widget.onPageScrollStart != null) {
widget.onPageScrollStart();
return true;
}
}
if (notification is ScrollEndNotification) {
if (widget.onPageScrollEnd != null) {
widget.onPageScrollEnd();
return true;
}
}
if (notification is ScrollUpdateNotification) {
final pixels = notification.metrics.pixels;
final maxScrollExtent = notification.metrics.maxScrollExtent;
final minScrollExtent = notification.metrics.minScrollExtent;
final scrollOffset = widget.scrollOffset;
if (pixels > (minScrollExtent + scrollOffset) &&
pixels < (maxScrollExtent - scrollOffset)) {
if (widget.onInBetweenOfPage != null) {
widget.onInBetweenOfPage();
return true;
}
}
final extentBefore = notification.metrics.extentBefore;
final extentAfter = notification.metrics.extentAfter;
final scrollingDown = _scrollPosition < pixels;
if (scrollOffset == null || scrollOffset == 0) {
if (extentAfter == 0) {
_onEndOfPage();
}
if (extentBefore == 0) {
_onStartOfPage();
}
} else {
if (scrollingDown) {
if (extentAfter <= scrollOffset) {
_onEndOfPage();
}
} else {
if (extentBefore <= scrollOffset) {
_onStartOfPage();
}
}
}
_scrollPosition = pixels;
return true;
}
if (notification is OverscrollNotification) {
if (notification.overscroll > 0) {
_onEndOfPage();
}
if (notification.overscroll < 0) {
_onStartOfPage();
}
return true;
}
return false;
}
void _onEndOfPage() {
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) {
_loadMoreStatus = _LoadingStatus.LOADING;
if (widget.onEndOfPage != null) {
widget.onEndOfPage().whenComplete(() {
_loadMoreStatus = _LoadingStatus.STABLE;
});
}
}
}
void _onStartOfPage() {
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) {
_loadMoreStatus = _LoadingStatus.LOADING;
if (widget.onStartOfPage != null) {
widget.onStartOfPage().whenComplete(() {
_loadMoreStatus = _LoadingStatus.STABLE;
});
}
}
}
}
-215
View File
@@ -1,215 +0,0 @@
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import '../stream_chat_flutter.dart';
extension on Duration {
String format() {
final s = '$this'.split('.')[0].padLeft(8, '0');
if (s.startsWith('00:')) {
return s.replaceFirst('00:', '');
}
return s;
}
}
class MediaListView extends StatefulWidget {
final List<String> selectedIds;
final void Function(AssetEntity media) onSelect;
const MediaListView({
Key key,
this.selectedIds = const [],
this.onSelect,
}) : super(key: key);
@override
_MediaListViewState createState() => _MediaListViewState();
}
class _MediaListViewState extends State<MediaListView> {
final _media = <AssetEntity>[];
final ScrollController _scrollController = ScrollController();
int _currentPage = 0;
@override
Widget build(BuildContext context) {
return LazyLoadScrollView(
onEndOfPage: () async => _getMedia(),
child: GridView.builder(
itemCount: _media.length,
controller: _scrollController,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
),
itemBuilder: (
context,
position,
) {
final media = _media.elementAt(position);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 1.0, vertical: 1.0),
child: InkWell(
child: Stack(
children: [
AspectRatio(
aspectRatio: 1.0,
child: FadeInImage(
fadeInDuration: Duration(milliseconds: 300),
placeholder: AssetImage(
'images/placeholder.png',
package: 'stream_chat_flutter',
),
image: MediaThumbnailProvider(
media: media,
),
fit: BoxFit.cover,
),
),
Positioned.fill(
child: IgnorePointer(
child: AnimatedOpacity(
duration: Duration(milliseconds: 300),
opacity: widget.selectedIds.any((id) => id == media.id)
? 1.0
: 0.0,
child: Container(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
alignment: Alignment.topRight,
padding: const EdgeInsets.only(
top: 8,
right: 8,
),
child: CircleAvatar(
radius: 12,
backgroundColor:
StreamChatTheme.of(context).colorTheme.white,
child: StreamSvgIcon.check(
size: 24,
color:
StreamChatTheme.of(context).colorTheme.black,
),
),
),
),
),
),
if (media.type == AssetType.video) ...[
Positioned(
left: 8,
bottom: 10,
child: SvgPicture.asset(
'svgs/video_call_icon.svg',
package: 'stream_chat_flutter',
),
),
Positioned(
right: 4,
bottom: 10,
child: Text(
media.videoDuration.format(),
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.white,
),
),
),
]
],
),
onTap: () {
if (widget.onSelect != null) {
widget.onSelect(media);
}
},
),
);
},
),
);
}
@override
void initState() {
super.initState();
_getMedia();
}
void _getMedia() async {
final assetList = await PhotoManager.getAssetPathList(
hasAll: true,
).then((value) {
if (value?.isNotEmpty == true) {
return value.singleWhere((element) => element.isAll);
}
});
if (assetList == null) {
return;
}
final media = await assetList.getAssetListPaged(_currentPage, 50);
if (!media.isEmpty) {
setState(() {
_media.addAll(media);
});
}
++_currentPage;
}
}
class MediaThumbnailProvider extends ImageProvider<MediaThumbnailProvider> {
const MediaThumbnailProvider({
@required this.media,
}) : assert(media != null);
final AssetEntity media;
@override
ImageStreamCompleter load(key, decode) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(key, decode),
scale: 1.0,
informationCollector: () sync* {
yield ErrorDescription('Id: ${media?.id}');
},
);
}
Future<ui.Codec> _loadAsync(
MediaThumbnailProvider key, DecoderCallback decode) async {
assert(key == this);
final bytes = await media.thumbData;
if (bytes?.isNotEmpty != true) return null;
return await decode(bytes);
}
@override
Future<MediaThumbnailProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<MediaThumbnailProvider>(this);
}
@override
bool operator ==(dynamic other) {
if (other.runtimeType != runtimeType) return false;
final MediaThumbnailProvider typedOther = other;
return media?.id == typedOther.media?.id;
}
@override
int get hashCode => media?.id?.hashCode ?? 0;
@override
String toString() => '$runtimeType("${media?.id}")';
}
-17
View File
@@ -1,17 +0,0 @@
import 'package:http_parser/http_parser.dart' as httpParser;
import 'package:mime/mime.dart';
class MediaUtils {
static httpParser.MediaType getMimeType(String filename) {
httpParser.MediaType mimeType;
if (filename != null) {
if (filename.toLowerCase().endsWith('heic')) {
mimeType = httpParser.MediaType.parse('image/heic');
} else {
mimeType = httpParser.MediaType.parse(lookupMimeType(filename));
}
}
return mimeType;
}
}
-392
View File
@@ -1,392 +0,0 @@
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/reaction_picker.dart';
import 'package:stream_chat_flutter/src/stream_channel.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'message_input.dart';
import 'message_widget.dart';
import 'stream_chat.dart';
import 'stream_chat_theme.dart';
class MessageActionsModal extends StatelessWidget {
final Widget Function(BuildContext, Message) editMessageInputBuilder;
final void Function(Message) onThreadReplyTap;
final void Function(Message) onReplyTap;
final Message message;
final MessageTheme messageTheme;
final bool showReactions;
final bool showDeleteMessage;
final bool showCopyMessage;
final bool showEditMessage;
final bool showResendMessage;
final bool showReply;
final bool showThreadReply;
final bool reverse;
final ShapeBorder messageShape;
final DisplayWidget showUserAvatar;
const MessageActionsModal({
Key key,
@required this.message,
@required this.messageTheme,
this.showReactions = true,
this.showDeleteMessage = true,
this.showEditMessage = true,
this.onReplyTap,
this.onThreadReplyTap,
this.showCopyMessage = true,
this.showReply = true,
this.showResendMessage = true,
this.showThreadReply = true,
this.showUserAvatar = DisplayWidget.show,
this.editMessageInputBuilder,
this.messageShape,
this.reverse = false,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final user = StreamChat.of(context).user;
final roughMaxSize = 2 * size.width / 3;
var messageTextLength = message.text.length;
if (message.quotedMessage != null) {
var quotedMessageLength = message.quotedMessage.text.length + 40;
if (message.quotedMessage.attachments?.isNotEmpty == true) {
quotedMessageLength += 40;
}
if (quotedMessageLength > messageTextLength) {
messageTextLength = quotedMessageLength;
}
}
final roughSentenceSize =
messageTextLength * messageTheme.messageText.fontSize * 1.2;
final divFactor = message.attachments?.isNotEmpty == true
? 1
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => Navigator.maybePop(context),
child: Stack(
children: [
Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 10,
sigmaY: 10,
),
child: Container(
color: StreamChatTheme.of(context).colorTheme.overlay,
),
),
),
Center(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
if (showReactions &&
(message.status == MessageSendingStatus.SENT ||
message.status == null))
Align(
alignment: Alignment(
user.id == message.user.id
? (divFactor > 1.0 ? 0.0 : (1.0 - divFactor))
: (divFactor > 1.0 ? 0.0 : -(1.0 - divFactor)),
0.0),
child: ReactionPicker(
message: message,
messageTheme: messageTheme,
),
),
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: Duration(milliseconds: 300),
builder: (context, val, snapshot) {
return Transform.scale(
scale: val,
child: IgnorePointer(
child: MessageWidget(
key: Key('MessageWidget'),
reverse: reverse,
message: message.copyWith(
text: message.text.length > 200
? '${message.text.substring(0, 200)}...'
: message.text,
),
messageTheme: messageTheme,
showReactions: false,
showUsername: false,
showThreadReplyIndicator: false,
showReplyIndicator: false,
showUserAvatar: showUserAvatar,
showTimestamp: false,
translateUserAvatar: false,
showReactionPickerIndicator: showReactions &&
(message.status ==
MessageSendingStatus.SENT ||
message.status == null),
showInChannelIndicator: false,
showSendingIndicator: DisplayWidget.gone,
shape: messageShape,
),
),
);
}),
SizedBox(height: 8),
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
builder: (context, val, wid) {
return Transform(
transform: Matrix4.identity()
..scale(val)
..rotateZ(-1.0 + val),
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 48.0,
),
child: Material(
color: StreamChatTheme.of(context)
.colorTheme
.whiteSnow,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.stretch,
children: ListTile.divideTiles(
context: context,
tiles: [
if (showReply &&
(message.status ==
MessageSendingStatus.SENT ||
message.status == null) &&
message.parentId == null)
_buildReplyButton(context),
if (showThreadReply &&
(message.status ==
MessageSendingStatus.SENT ||
message.status == null) &&
message.parentId == null)
_buildThreadReplyButton(context),
if (showResendMessage)
_buildResendMessage(context),
if (showEditMessage)
_buildEditMessage(context),
if (showDeleteMessage)
_buildDeleteButton(context),
if (showCopyMessage)
_buildCopyButton(context),
],
).toList(),
),
),
),
);
})
],
),
),
),
),
],
),
);
}
Widget _buildReplyButton(BuildContext context) {
return ListTile(
title: Text(
'Reply',
style: Theme.of(context).textTheme.headline6,
),
leading: StreamSvgIcon.reply(
color: StreamChatTheme.of(context).primaryIconTheme.color,
),
onTap: () {
Navigator.pop(context);
if (onReplyTap != null) {
onReplyTap(message);
}
},
);
}
Widget _buildDeleteButton(BuildContext context) {
final isDeleteFailed = message.status == MessageSendingStatus.FAILED_DELETE;
return ListTile(
title: Text(
isDeleteFailed ? 'Retry deleting message' : 'Delete message',
style:
Theme.of(context).textTheme.headline6.copyWith(color: Colors.red),
),
leading: StreamSvgIcon.delete(
color: Colors.red,
),
onTap: () {
Navigator.pop(context);
StreamChat.of(context).client.deleteMessage(
message,
StreamChannel.of(context).channel.cid,
);
},
);
}
Widget _buildCopyButton(BuildContext context) {
return ListTile(
title: Text(
'Copy message',
style: Theme.of(context).textTheme.headline6,
),
leading: StreamSvgIcon.copy(
color: StreamChatTheme.of(context).primaryIconTheme.color,
),
onTap: () async {
await Clipboard.setData(ClipboardData(text: message.text));
Navigator.pop(context);
},
);
}
Widget _buildEditMessage(BuildContext context) {
return ListTile(
title: Text(
'Edit message',
style: Theme.of(context).textTheme.headline6,
),
leading: StreamSvgIcon.edit(
color: StreamChatTheme.of(context).primaryIconTheme.color,
),
onTap: () async {
Navigator.pop(context);
_showEditBottomSheet(context);
},
);
}
Widget _buildResendMessage(BuildContext context) {
final isUpdateFailed = message.status == MessageSendingStatus.FAILED_UPDATE;
return ListTile(
title: Text(
isUpdateFailed ? 'Resend edited message' : 'Resend',
style: Theme.of(context).textTheme.headline6,
),
leading: StreamSvgIcon.circle_up(
color: StreamChatTheme.of(context).colorTheme.accentBlue,
),
onTap: () {
Navigator.pop(context);
final client = StreamChat.of(context).client;
final channel = StreamChannel.of(context).channel;
if (isUpdateFailed) {
client.updateMessage(message, channel.cid);
} else {
channel.sendMessage(message);
}
},
);
}
void _showEditBottomSheet(BuildContext context) {
final channel = StreamChannel.of(context).channel;
showModalBottomSheet(
context: context,
elevation: 2,
clipBehavior: Clip.hardEdge,
isScrollControlled: true,
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
builder: (context) {
return StreamChannel(
channel: channel,
child: Flex(
direction: Axis.vertical,
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Padding(
padding: const EdgeInsets.fromLTRB(8, 8, 8, 0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: StreamSvgIcon.edit(
color: StreamChatTheme.of(context)
.colorTheme
.greyGainsboro,
),
),
Text(
'Edit Message',
style: TextStyle(fontWeight: FontWeight.bold),
),
IconButton(
visualDensity: VisualDensity.compact,
icon: StreamSvgIcon.close_small(),
onPressed: Navigator.of(context).pop,
),
],
),
),
Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: editMessageInputBuilder != null
? editMessageInputBuilder(context, message)
: MessageInput(
editMessage: message,
preMessageSending: (m) {
FocusScope.of(context).unfocus();
Navigator.pop(context);
return m;
},
),
),
],
),
);
},
);
}
Widget _buildThreadReplyButton(BuildContext context) {
return ListTile(
title: Text(
'Thread reply',
style: Theme.of(context).textTheme.headline6,
),
leading: StreamSvgIcon.thread(
color: StreamChatTheme.of(context).primaryIconTheme.color,
),
onTap: () {
Navigator.pop(context);
if (onThreadReplyTap != null) {
onThreadReplyTap(message);
}
},
);
}
}
File diff suppressed because it is too large Load Diff
-940
View File
@@ -1,940 +0,0 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:rxdart/rxdart.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart';
import 'package:stream_chat_flutter/src/message_widget.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/system_message.dart';
import 'package:visibility_detector/visibility_detector.dart';
import '../stream_chat_flutter.dart';
import 'date_divider.dart';
import 'stream_channel.dart';
import 'swipeable.dart';
typedef MessageBuilder = Widget Function(
BuildContext,
MessageDetails,
List<Message>,
);
typedef ParentMessageBuilder = Widget Function(
BuildContext,
Message,
);
typedef ThreadBuilder = Widget Function(BuildContext context, Message parent);
typedef ThreadTapCallback = void Function(Message, Widget);
typedef OnMessageSwiped = void Function(Message);
typedef ReplyTapCallback = void Function(Message);
class MessageDetails {
/// True if the message belongs to the current user
bool isMyMessage;
/// True if the user message is the same of the previous message
bool isLastUser;
/// True if the user message is the same of the next message
bool isNextUser;
/// The message
Message message;
/// The index of the message
int index;
MessageDetails(
BuildContext context,
this.message,
List<Message> messages,
this.index,
) {
isMyMessage = message.user.id == StreamChat.of(context).user.id;
isLastUser = index + 1 < messages.length &&
message.user.id == messages[index + 1]?.user?.id;
isNextUser =
index - 1 >= 0 && message.user.id == messages[index - 1]?.user?.id;
}
}
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_listview.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_listview_paint.png)
///
/// It shows the list of messages of the current channel.
///
/// ```dart
/// class ChannelPage extends StatelessWidget {
/// const ChannelPage({
/// Key key,
/// }) : super(key: key);
///
/// @override
/// Widget build(BuildContext context) {
/// return Scaffold(
/// appBar: ChannelHeader(),
/// body: Column(
/// children: <Widget>[
/// Expanded(
/// child: MessageListView(
/// threadBuilder: (_, parentMessage) {
/// return ThreadPage(
/// parent: parentMessage,
/// );
/// },
/// ),
/// ),
/// MessageInput(),
/// ],
/// ),
/// );
/// }
/// }
/// ```
///
///
/// Make sure to have a [StreamChannel] ancestor in order to provide the information about the channels.
/// The widget uses a [ListView.custom] to render the list of channels.
///
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme].
/// Modify it to change the widget appearance.
class MessageListView extends StatefulWidget {
/// Instantiate a new MessageListView
MessageListView({
Key key,
this.showScrollToBottom = true,
this.messageBuilder,
this.parentMessageBuilder,
this.parentMessage,
this.threadBuilder,
this.onThreadTap,
this.onReplyTap,
this.dateDividerBuilder,
this.scrollPhysics = const ClampingScrollPhysics(),
this.initialScrollIndex,
this.initialAlignment,
this.scrollController,
this.itemPositionListener,
this.onMessageSwiped,
this.highlightInitialMessage = false,
this.messageHighlightColor,
this.onShowMessage,
}) : super(key: key);
/// Function used to build a custom message widget
final MessageBuilder messageBuilder;
/// Function used to build a custom parent message widget
final ParentMessageBuilder parentMessageBuilder;
/// Function used to build a custom thread widget
final ThreadBuilder threadBuilder;
/// Function called when tapping on a thread
/// By default it calls [Navigator.push] using the widget built using [threadBuilder]
final ThreadTapCallback onThreadTap;
/// If true will show a scroll to bottom message when there are new messages and the scroll offset is not zero
final bool showScrollToBottom;
/// Parent message in case of a thread
final Message parentMessage;
/// Builder used to render date dividers
final Widget Function(DateTime) dateDividerBuilder;
/// Index of an item to initially align within the viewport.
final int initialScrollIndex;
/// Determines where the leading edge of the item at [initialScrollIndex]
/// should be placed.
final double initialAlignment;
/// Controller for jumping or scrolling to an item.
final ItemScrollController scrollController;
/// Provides a listenable iterable of [itemPositions] of items that are on
/// screen and their locations.
final ItemPositionsListener itemPositionListener;
/// The ScrollPhysics used by the ListView
final ScrollPhysics scrollPhysics;
/// Called when message item gets swiped
final OnMessageSwiped onMessageSwiped;
///
final ReplyTapCallback onReplyTap;
/// If true the list will highlight the initialMessage if there is any.
///
/// Also See [StreamChannel]
final bool highlightInitialMessage;
/// Color used while highlighting initial message
final Color messageHighlightColor;
final ShowMessageCallback onShowMessage;
@override
_MessageListViewState createState() => _MessageListViewState();
}
class _MessageListViewState extends State<MessageListView> {
ItemScrollController _scrollController;
bool _bottomWasVisible = false;
Function _onThreadTap;
bool _showScrollToBottom = false;
ItemPositionsListener _itemPositionListener;
int _messageListLength;
StreamChannelState streamChannel;
int get _initialIndex {
if (widget.initialScrollIndex != null) return widget.initialScrollIndex;
if (streamChannel.initialMessageId != null) {
final messages = streamChannel.channel.state.messages;
final totalMessages = messages.length;
final messageIndex = messages.indexWhere((e) {
return e.id == streamChannel.initialMessageId;
});
final index = totalMessages - messageIndex;
if (index != 0) return index - 1;
return index;
}
return 0;
}
double get _initialAlignment {
if (widget.initialAlignment != null) return widget.initialAlignment;
return 0;
}
bool _isInitialMessage(String id) {
return streamChannel.initialMessageId == id;
}
bool get _upToDate => streamChannel.channel.state.isUpToDate;
bool _topPaginationActive = false;
bool _bottomPaginationActive = false;
int initialIndex;
double initialAlignment;
List<Message> messages = <Message>[];
bool initialMessageHighlightComplete = false;
bool _inBetweenList = false;
@override
Widget build(BuildContext context) {
final messagesStream = widget.parentMessage != null
? streamChannel.channel.state.threadsStream
.where((threads) => threads.containsKey(widget.parentMessage.id))
.map((threads) => threads[widget.parentMessage.id])
: streamChannel.channel.state?.messagesStream;
return StreamBuilder<List<Message>>(
stream: messagesStream?.map((messages) => messages
?.where((e) =>
(!e.isDeleted && e.shadowed != true) ||
(e.isDeleted &&
e.user.id == streamChannel.channel.client.state.user.id))
?.toList()),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: const CircularProgressIndicator(),
);
}
final messageList = snapshot.data?.reversed?.toList() ?? [];
if (messageList.isEmpty) {
if (_upToDate) {
return Center(
child: Text(
'No chats here yet...',
style: StreamChatTheme.of(context)
.textTheme
.footnote
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5)),
),
);
}
} else {
messages = messageList;
}
final newMessagesListLength = messages.length;
if (_messageListLength != null) {
if (_bottomPaginationActive || (_inBetweenList && _upToDate)) {
if (_itemPositionListener.itemPositions.value?.isNotEmpty ==
true) {
final first = _itemPositionListener.itemPositions.value.first;
final diff = newMessagesListLength - _messageListLength;
if (diff > 0) {
initialIndex = first.index + diff;
initialAlignment = first.itemLeadingEdge;
}
}
} else if (!_topPaginationActive && _upToDate) {
// Reset the index in-case we send any new message
initialIndex = 0;
initialAlignment = 0;
}
}
_messageListLength = newMessagesListLength;
return Stack(
alignment: Alignment.center,
children: [
LazyLoadScrollView(
onStartOfPage: () async {
_inBetweenList = false;
if (!_upToDate) {
_topPaginationActive = false;
_bottomPaginationActive = true;
return _paginateData(
streamChannel,
QueryDirection.bottom,
);
}
},
onEndOfPage: () async {
_inBetweenList = false;
_topPaginationActive = true;
_bottomPaginationActive = false;
return _paginateData(
streamChannel,
QueryDirection.top,
);
},
onInBetweenOfPage: () {
_inBetweenList = true;
},
child: ScrollablePositionedList.builder(
key: ValueKey(initialIndex + initialAlignment),
itemPositionsListener: _itemPositionListener,
addAutomaticKeepAlives: true,
initialScrollIndex: initialIndex ?? 0,
initialAlignment: initialAlignment ?? 0,
physics: widget.scrollPhysics,
itemScrollController: _scrollController,
reverse: true,
itemCount: messages.length +
2 +
(widget.parentMessage != null ? 1 : 0),
itemBuilder: (context, i) {
if (i == messages.length + 2) {
if (widget.parentMessageBuilder != null) {
return widget.parentMessageBuilder(
context,
widget.parentMessage,
);
} else {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
buildParentMessage(widget.parentMessage),
Container(
decoration: BoxDecoration(
gradient: StreamChatTheme.of(context)
.colorTheme
.bgGradient,
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'${widget.parentMessage.replyCount} ${widget.parentMessage.replyCount == 1 ? 'Reply' : 'Replies'}',
textAlign: TextAlign.center,
style: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.lastMessageAt,
),
),
),
],
);
}
}
if (i == messages.length + 1) {
return _buildLoadingIndicator(
streamChannel,
QueryDirection.top,
);
}
if (i == 0) {
return _buildLoadingIndicator(
streamChannel,
QueryDirection.bottom,
);
}
final message = messages[i - 1];
final nextMessage = (i - 1) > 0 ? messages[i - 2] : null;
Widget messageWidget;
if (i == 1) {
messageWidget = _buildBottomMessage(
context,
message,
messages,
streamChannel,
);
} else if (i == messages.length - 1) {
messageWidget = _buildTopMessage(
context,
message,
messages,
streamChannel,
);
} else {
if (widget.messageBuilder != null) {
messageWidget = Builder(
key: ValueKey<String>('MESSAGE-${message.id}'),
builder: (context) => widget.messageBuilder(
context,
MessageDetails(
context,
message,
messages,
i,
),
messages),
);
} else {
messageWidget = buildMessage(message, messages, i);
}
}
if (nextMessage != null &&
!Jiffy(message.createdAt.toLocal()).isSame(
nextMessage.createdAt.toLocal(), Units.DAY)) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
messageWidget,
Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0),
child: widget.dateDividerBuilder != null
? widget.dateDividerBuilder(
nextMessage.createdAt.toLocal())
: DateDivider(
dateTime: nextMessage.createdAt.toLocal(),
),
),
],
);
}
return messageWidget;
},
),
),
if (widget.showScrollToBottom) _buildScrollToBottom(),
Positioned(
top: 20.0,
child: ValueListenableBuilder<Iterable<ItemPosition>>(
valueListenable: _itemPositionListener.itemPositions,
builder: (context, values, _) {
final items = _itemPositionListener.itemPositions?.value;
if (items.isEmpty || messages.isEmpty) {
return SizedBox();
}
var index = _getTopElement(values).index;
if (index > messages.length) {
return SizedBox();
}
if (index == messages.length) {
index = max(index - 1, 0);
}
return widget.dateDividerBuilder != null
? widget.dateDividerBuilder(
messages[index].createdAt.toLocal(),
)
: DateDivider(
dateTime: messages[index].createdAt.toLocal(),
);
},
),
),
],
);
});
}
Future<void> _paginateData(
StreamChannelState channel, QueryDirection direction) {
if (widget.parentMessage == null) {
return channel.queryMessages(direction: direction);
} else {
return channel.getReplies(widget.parentMessage.id);
}
}
ItemPosition _getTopElement(Iterable<ItemPosition> values) {
return values
.where((ItemPosition position) => position.itemLeadingEdge < 0.9)
.reduce((ItemPosition max, ItemPosition position) =>
position.itemLeadingEdge > max.itemLeadingEdge ? position : max);
}
Widget _buildScrollToBottom() {
return StreamBuilder<Tuple2<bool, int>>(
stream: Rx.combineLatest2(
streamChannel.channel.state.isUpToDateStream,
streamChannel.channel.state.unreadCountStream,
(bool isUpToDate, int unreadCount) => Tuple2(isUpToDate, unreadCount),
),
builder: (_, snapshot) {
if (snapshot.hasError) {
return Offstage();
} else if (!snapshot.hasData) {
return Offstage();
}
final isUpToDate = snapshot.data.item1;
final showScrollToBottom = !isUpToDate || _showScrollToBottom;
if (!showScrollToBottom) {
return Offstage();
}
final unreadCount = snapshot.data.item2;
final showUnreadCount = unreadCount > 0 &&
streamChannel.channel.state.members.any(
(e) => e.userId == streamChannel.channel.client.state.user.id);
return Positioned(
bottom: 8,
right: 8,
width: 40,
height: 40,
child: Stack(
clipBehavior: Clip.none,
children: [
FloatingActionButton(
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
child: StreamSvgIcon.down(
color: StreamChatTheme.of(context).colorTheme.black,
),
onPressed: () {
if (unreadCount > 0) {
streamChannel.channel.markRead();
}
if (!_upToDate) {
_bottomPaginationActive = false;
_topPaginationActive = false;
streamChannel.reloadChannel();
} else {
setState(() => _showScrollToBottom = false);
_scrollController.scrollTo(
index: 0,
duration: Duration(seconds: 1),
curve: Curves.easeInOut,
);
}
},
),
if (showUnreadCount)
Positioned(
width: 20,
height: 20,
left: 10,
top: -10,
child: CircleAvatar(
child: Padding(
padding: const EdgeInsets.all(3.0),
child: Text(
'$unreadCount',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),
);
},
);
}
Widget _buildLoadingIndicator(
StreamChannelState streamChannel,
QueryDirection direction,
) {
final stream = direction == QueryDirection.top
? streamChannel.queryTopMessages
: streamChannel.queryBottomMessages;
return StreamBuilder<bool>(
key: Key('LOADING-INDICATOR'),
stream: stream,
initialData: false,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Container(
color: StreamChatTheme.of(context)
.colorTheme
.accentRed
.withOpacity(.2),
child: Center(
child: Text('Error loading messages'),
),
);
}
if (!snapshot.data) {
if (direction == QueryDirection.top) {
return Container(
height: 52,
width: double.infinity,
);
}
return Offstage();
}
return Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: const CircularProgressIndicator(),
),
);
});
}
Widget _buildTopMessage(
BuildContext context,
Message message,
List<Message> messages,
StreamChannelState streamChannel,
) {
Widget messageWidget;
if (widget.messageBuilder != null) {
messageWidget = Builder(
key: ValueKey<String>('TOP-MESSAGE'),
builder: (_) => widget.messageBuilder(
context,
MessageDetails(
context,
message,
messages,
messages.length - 1,
),
messages,
),
);
} else {
messageWidget = buildMessage(message, messages, messages.length - 1);
}
return messageWidget;
}
Widget _buildBottomMessage(
BuildContext context,
Message message,
List<Message> messages,
StreamChannelState streamChannel,
) {
Widget messageWidget;
if (widget.messageBuilder != null) {
messageWidget = Builder(
key: ValueKey<String>('BOTTOM-MESSAGE'),
builder: (_) => widget.messageBuilder(
context,
MessageDetails(
context,
message,
messages,
0,
),
messages,
),
);
} else {
messageWidget = buildMessage(message, messages, 0);
}
return VisibilityDetector(
key: ValueKey<String>('BOTTOM-MESSAGE'),
onVisibilityChanged: (visibility) {
final isVisible = visibility.visibleBounds != Rect.zero;
if (isVisible && !_bottomWasVisible) {
final channel = streamChannel.channel;
if (_upToDate &&
channel.config?.readEvents == true &&
channel.state.unreadCount > 0) {
streamChannel.channel.markRead();
}
_bottomWasVisible = !isVisible;
}
if (mounted) {
setState(() => _showScrollToBottom = !isVisible);
}
},
child: messageWidget,
);
}
Widget buildParentMessage(
Message message,
) {
final isMyMessage = message.user.id == StreamChat.of(context).user.id;
return MessageWidget(
showThreadReplyIndicator: false,
showInChannelIndicator: false,
showReplyIndicator: false,
message: message,
reverse: isMyMessage,
showUsername: !isMyMessage,
padding: EdgeInsets.only(
top: 8.0,
left: 8.0,
right: 8.0,
bottom: 16.0,
),
showSendingIndicator: DisplayWidget.hide,
onThreadTap: _onThreadTap,
showEditMessage: false,
showDeleteMessage: false,
borderRadiusGeometry: BorderRadius.only(
topLeft: Radius.circular(16),
bottomLeft: Radius.circular(2),
topRight: Radius.circular(16),
bottomRight: Radius.circular(16),
),
borderSide: isMyMessage ? BorderSide.none : null,
showUserAvatar: isMyMessage ? DisplayWidget.gone : DisplayWidget.show,
messageTheme: isMyMessage
? StreamChatTheme.of(context).ownMessageTheme
: StreamChatTheme.of(context).otherMessageTheme,
onShowMessage: widget.onShowMessage,
);
}
Widget buildMessage(
Message message,
List<Message> messages,
int index,
) {
if (message.type == 'system' && message.text?.isNotEmpty == true) {
return SystemMessage(
key: ValueKey<String>('MESSAGE-${message.id}'),
message: message,
);
}
final userId = StreamChat.of(context).user.id;
final isMyMessage = message.user.id == userId;
final isNextUser =
index - 2 >= 0 && message.user.id == messages[index - 2]?.user?.id;
final channel = streamChannel.channel;
final readList = channel.state?.read
?.where((element) => element.user.id != userId)
?.where((read) =>
(read.lastRead.isAfter(message.createdAt) ||
read.lastRead.isAtSameMomentAs(message.createdAt)) &&
(index == 0 ||
read.lastRead.isBefore(messages[index - 1].createdAt)))
?.toList() ??
[];
final allRead = readList.length >= (channel.memberCount ?? 0) - 1;
final isThreadMessage =
widget.parentMessage != null || message?.showInChannel == true;
Widget child = MessageWidget(
key: ValueKey<String>('MESSAGE-${message.id}'),
message: message,
reverse: isMyMessage,
showReactions: !message.isDeleted,
padding: EdgeInsets.only(
left: 8.0,
right: 8.0,
bottom: index == 0 ? 30 : (isNextUser ? 2 : 7),
top: 3,
),
onQuotedMessageTap: (quotedMessageId) async {
final scrollToIndex = () {
final index = messages.indexWhere((m) => m.id == quotedMessageId);
_scrollController?.scrollTo(
index: index,
duration: const Duration(milliseconds: 350),
);
};
if (messages.map((e) => e.id).contains(quotedMessageId)) {
scrollToIndex();
} else {
streamChannel.loadChannelAtMessage(quotedMessageId).then((_) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (messages.map((e) => e.id).contains(quotedMessageId)) {
scrollToIndex();
}
});
});
}
},
showInChannelIndicator: widget.parentMessage == null,
showThreadReplyIndicator: widget.parentMessage == null,
showUsername: !isMyMessage && !isNextUser,
showSendingIndicator: isMyMessage &&
(index == 0 || message.status != MessageSendingStatus.SENT)
? DisplayWidget.show
: DisplayWidget.hide,
showTimestamp: !isNextUser || readList?.isNotEmpty == true,
showEditMessage: isMyMessage,
showDeleteMessage: isMyMessage,
borderSide: isMyMessage ? BorderSide.none : null,
onThreadTap: _onThreadTap,
onReplyTap: widget.onReplyTap,
attachmentBorderRadiusGeometry: BorderRadius.only(
topLeft: Radius.circular(16),
bottomLeft: Radius.circular(!isNextUser ? 0 : 16),
topRight: Radius.circular(16),
bottomRight: Radius.circular(16),
),
attachmentPadding: const EdgeInsets.all(2),
borderRadiusGeometry: BorderRadius.only(
topLeft: Radius.circular(16),
bottomLeft: Radius.circular(!isNextUser ? 0 : 16),
topRight: Radius.circular(16),
bottomRight: Radius.circular(16),
),
showUserAvatar: isMyMessage
? DisplayWidget.gone
: (isNextUser ? DisplayWidget.hide : DisplayWidget.show),
messageTheme: isMyMessage
? StreamChatTheme.of(context).ownMessageTheme
: StreamChatTheme.of(context).otherMessageTheme,
readList: readList,
allRead: allRead,
onShowMessage: widget.onShowMessage,
);
if (!isThreadMessage) {
child = Swipeable(
onSwipeEnd: () => widget.onMessageSwiped(message),
backgroundIcon: StreamSvgIcon.reply(
color: StreamChatTheme.of(context).colorTheme.accentBlue,
),
child: child,
);
}
if (!initialMessageHighlightComplete &&
widget.highlightInitialMessage &&
_isInitialMessage(message.id)) {
final colorTheme = StreamChatTheme.of(context).colorTheme;
final highlightColor =
widget.messageHighlightColor ?? colorTheme.highlight;
child = TweenAnimationBuilder<Color>(
tween: ColorTween(
begin: highlightColor,
end: colorTheme.white.withOpacity(0),
),
duration: const Duration(seconds: 3),
child: child,
onEnd: () => initialMessageHighlightComplete = true,
builder: (_, color, child) {
return Container(
color: color,
child: child,
);
},
);
}
return child;
}
StreamSubscription _messageNewListener;
@override
void initState() {
_scrollController = widget.scrollController ?? ItemScrollController();
_itemPositionListener =
widget.itemPositionListener ?? ItemPositionsListener.create();
streamChannel = StreamChannel.of(context);
initialIndex = _initialIndex;
initialAlignment = _initialAlignment;
_messageNewListener =
streamChannel.channel.on(EventType.messageNew).listen((event) {
if (_upToDate) {
_bottomPaginationActive = false;
_topPaginationActive = false;
}
if (event.message.user.id == streamChannel.channel.client.state.user.id) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollController?.jumpTo(
index: 0,
);
});
}
});
if (widget.parentMessage != null) {
streamChannel.getReplies(widget.parentMessage.id);
}
_getOnThreadTap();
super.initState();
}
void _getOnThreadTap() {
if (widget.onThreadTap != null) {
_onThreadTap = (Message message) {
widget.onThreadTap(
message,
widget.threadBuilder != null
? widget.threadBuilder(context, message)
: null);
};
} else if (widget.threadBuilder != null) {
_onThreadTap = (Message message) {
Navigator.push(
context,
MaterialPageRoute(builder: (_) {
return StreamBuilder<Message>(
stream: streamChannel.channel.state.messagesStream.map(
(messages) =>
messages.firstWhere((m) => m.id == message.id)),
initialData: message,
builder: (_, snapshot) {
return StreamChannel(
channel: streamChannel.channel,
child: widget.threadBuilder(context, snapshot.data),
);
});
}),
);
};
}
}
@override
void dispose() {
if (!_upToDate) {
streamChannel.reloadChannel();
}
_messageNewListener?.cancel();
super.dispose();
}
}
-256
View File
@@ -1,256 +0,0 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/reaction_bubble.dart';
import 'package:stream_chat_flutter/src/reaction_picker.dart';
import 'package:stream_chat_flutter/src/stream_chat.dart';
import 'package:stream_chat_flutter/src/user_avatar.dart';
import 'message_widget.dart';
import 'stream_chat_theme.dart';
class MessageReactionsModal extends StatelessWidget {
final Widget Function(BuildContext, Message) editMessageInputBuilder;
final void Function(Message) onThreadTap;
final Message message;
final MessageTheme messageTheme;
final bool reverse;
final bool showReactions;
final DisplayWidget showUserAvatar;
final ShapeBorder messageShape;
final void Function(User) onUserAvatarTap;
const MessageReactionsModal({
Key key,
@required this.message,
@required this.messageTheme,
this.showReactions = true,
this.onThreadTap,
this.editMessageInputBuilder,
this.messageShape,
this.reverse = false,
this.showUserAvatar = DisplayWidget.show,
this.onUserAvatarTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final user = StreamChat.of(context).user;
final roughMaxSize = 2 * size.width / 3;
var messageTextLength = message.text.length;
if (message.quotedMessage != null) {
var quotedMessageLength = message.quotedMessage.text.length + 40;
if (message.quotedMessage.attachments?.isNotEmpty == true) {
quotedMessageLength += 40;
}
if (quotedMessageLength > messageTextLength) {
messageTextLength = quotedMessageLength;
}
}
final roughSentenceSize =
messageTextLength * messageTheme.messageText.fontSize * 1.2;
final divFactor = message.attachments?.isNotEmpty == true
? 1
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => Navigator.maybePop(context),
child: Stack(
children: [
Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 10,
sigmaY: 10,
),
child: Container(
color: StreamChatTheme.of(context).colorTheme.overlay,
),
),
),
Center(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
if (showReactions &&
(message.status == MessageSendingStatus.SENT ||
message.status == null))
Align(
alignment: Alignment(
user.id == message.user.id
? (divFactor > 1.0 ? 0.0 : (1.0 - divFactor))
: (divFactor > 1.0 ? 0.0 : -(1.0 - divFactor)),
0.0),
child: ReactionPicker(
message: message,
messageTheme: messageTheme,
),
),
TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: Duration(milliseconds: 300),
builder: (context, val, snapshot) {
return Transform.scale(
scale: val,
child: IgnorePointer(
child: MessageWidget(
key: Key('MessageWidget'),
reverse: reverse,
message: message.copyWith(
text: message.text.length > 200
? '${message.text.substring(0, 200)}...'
: message.text,
),
messageTheme: messageTheme,
showReactions: false,
showUsername: false,
showUserAvatar: showUserAvatar,
showThreadReplyIndicator: false,
showTimestamp: false,
translateUserAvatar: false,
showSendingIndicator: DisplayWidget.gone,
shape: messageShape,
showInChannelIndicator: false,
showReactionPickerIndicator: showReactions &&
(message.status ==
MessageSendingStatus.SENT ||
message.status == null),
),
),
);
}),
SizedBox(height: 8),
if (message.latestReactions?.isNotEmpty == true)
_buildReactionCard(context),
],
),
),
),
),
],
),
);
}
Padding _buildReactionCard(BuildContext context) {
final currentUser = StreamChat.of(context).user;
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,
),
child: Card(
color: StreamChatTheme.of(context).colorTheme.white,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Message Reactions',
style: Theme.of(context).textTheme.headline6,
),
),
Flexible(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(
left: 18,
right: 18,
bottom: 26,
),
child: Wrap(
spacing: 16,
runSpacing: 22,
alignment: WrapAlignment.start,
children: message.latestReactions
.map((e) => _buildReaction(
e,
currentUser,
context,
))
.toList(),
),
),
),
),
],
),
),
);
}
Widget _buildReaction(
Reaction reaction,
User currentUser,
BuildContext context,
) {
final isCurrentUser = reaction.user.id == currentUser.id;
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
builder: (context, val, snapshot) {
return Transform.scale(
scale: val,
child: ConstrainedBox(
constraints: BoxConstraints.loose(Size(
64,
98,
)),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Stack(
children: [
UserAvatar(
onTap: onUserAvatarTap,
user: reaction.user,
constraints: BoxConstraints.tightFor(
height: 64,
width: 64,
),
borderRadius: BorderRadius.circular(32),
),
Positioned(
child: Align(
alignment: Alignment.centerLeft,
child: ReactionBubble(
reactions: [reaction],
borderColor: messageTheme.reactionsBorderColor,
backgroundColor:
messageTheme.reactionsBackgroundColor,
highlightOwnReactions: false,
),
),
bottom: 4,
left: isCurrentUser ? 0 : null,
right: isCurrentUser ? 0 : null,
),
],
),
Text(
reaction.user.name,
style: Theme.of(context).textTheme.subtitle2,
textAlign: TextAlign.center,
),
],
),
),
);
});
}
}
-113
View File
@@ -1,113 +0,0 @@
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/stream_chat.dart';
import 'stream_chat.dart';
/// Widget dedicated to the management of a message list with pagination
class MessageSearchBloc extends StatefulWidget {
/// The widget child
final Widget child;
/// Instantiate a new MessageSearchBloc
const MessageSearchBloc({
Key key,
@required this.child,
}) : super(key: key);
@override
MessageSearchBlocState createState() => MessageSearchBlocState();
/// Use this method to get the current [MessageSearchBlocState] instance
static MessageSearchBlocState of(BuildContext context) {
MessageSearchBlocState state;
state = context.findAncestorStateOfType<MessageSearchBlocState>();
if (state == null) {
throw Exception('You must have a MessageSearchBloc widget as ancestor');
}
return state;
}
}
/// The current state of the [MessageSearchBloc]
class MessageSearchBlocState extends State<MessageSearchBloc>
with AutomaticKeepAliveClientMixin {
/// The current messages list
List<GetMessageResponse> get messageResponses => _messageResponses.value;
/// The current messages list as a stream
Stream<List<GetMessageResponse>> get messagesStream =>
_messageResponses.stream;
final BehaviorSubject<List<GetMessageResponse>> _messageResponses =
BehaviorSubject();
final BehaviorSubject<bool> _queryMessagesLoadingController =
BehaviorSubject.seeded(false);
/// The stream notifying the state of queryUsers call
Stream<bool> get queryMessagesLoading =>
_queryMessagesLoadingController.stream;
/// Calls [Client.search] updating [queryMessagesLoading] stream
Future<void> search({
Map<String, dynamic> filter,
Map<String, dynamic> messageFilter,
List<SortOption> sort,
String query,
PaginationParams pagination,
}) async {
final client = StreamChat.of(context).client;
if (client.state?.user == null ||
_queryMessagesLoadingController.value == true) {
return;
}
_queryMessagesLoadingController.add(true);
try {
final clear = pagination == null ||
pagination.offset == null ||
pagination.offset == 0;
final oldMessages = List<GetMessageResponse>.from(messageResponses ?? []);
final messageResponse = await client.search(
filter,
sort,
query,
pagination,
messageFilters: messageFilter,
);
if (clear) {
_messageResponses.add(messageResponse.results);
} else {
final temp = oldMessages + messageResponse.results;
_messageResponses.add(temp);
}
_queryMessagesLoadingController.add(false);
} catch (err, stackTrace) {
_queryMessagesLoadingController.addError(err, stackTrace);
}
}
@override
Widget build(BuildContext context) {
super.build(context);
return widget.child;
}
@override
void dispose() {
_messageResponses.close();
_queryMessagesLoadingController.close();
super.dispose();
}
@override
bool get wantKeepAlive => true;
}
-183
View File
@@ -1,183 +0,0 @@
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// It shows the current [Message] preview.
///
/// Usually you don't use this widget as it's the default item used by [MessageSearchListView].
///
/// The widget renders the ui based on the first ancestor of type [StreamChatTheme].
/// Modify it to change the widget appearance.
class MessageSearchItem extends StatelessWidget {
/// Instantiate a new MessageSearchItem
const MessageSearchItem({
Key key,
@required this.getMessageResponse,
this.onTap,
this.showOnlineStatus = true,
}) : super(key: key);
/// [Message] displayed
final GetMessageResponse getMessageResponse;
/// Function called when tapping this widget
final VoidCallback onTap;
/// If true the [MessageSearchItem] will show the current online Status
final bool showOnlineStatus;
@override
Widget build(BuildContext context) {
final message = getMessageResponse.message;
final channel = getMessageResponse.channel;
final channelName = channel.extraData['name'];
final user = message.user;
return ListTile(
onTap: onTap,
leading: UserAvatar(
user: user,
showOnlineStatus: showOnlineStatus,
constraints: BoxConstraints.tightFor(
height: 40,
width: 40,
),
),
title: Row(
children: [
Text(
user.id == StreamChat.of(context).user.id ? 'You' : user.name,
style: StreamChatTheme.of(context).channelPreviewTheme.title,
),
if (channelName != null) ...[
Text(
' in ',
style: StreamChatTheme.of(context)
.channelPreviewTheme
.title
.copyWith(
fontWeight: FontWeight.normal,
),
),
Text(
channelName,
style: StreamChatTheme.of(context).channelPreviewTheme.title,
),
],
],
),
subtitle: Row(
children: [
Expanded(child: _buildSubtitle(context, message)),
SizedBox(width: 16),
_buildDate(context, message),
],
),
);
}
Widget _buildDate(BuildContext context, Message message) {
final createdAt = message.createdAt;
String stringDate;
final now = DateTime.now();
if (now.year != createdAt.year ||
now.month != createdAt.month ||
now.day != createdAt.day) {
stringDate = Jiffy(createdAt.toLocal()).format('dd/MM/yyyy');
} else {
stringDate = Jiffy(createdAt.toLocal()).format('HH:mm');
}
return Text(
stringDate,
style: StreamChatTheme.of(context).channelPreviewTheme.lastMessageAt,
);
}
Widget _buildSubtitle(BuildContext context, Message message) {
if (message == null) {
return SizedBox();
}
var text = message.text;
if (message.isDeleted) {
text = 'This message was deleted.';
} else if (message.attachments != null) {
final parts = <String>[
...message.attachments.map((e) {
if (e.type == 'image') {
return '📷';
} else if (e.type == 'video') {
return '🎬';
} else if (e.type == 'giphy') {
return '[GIF]';
}
return e == message.attachments.last
? (e.title ?? 'File')
: '${e.title ?? 'File'} , ';
}).where((e) => e != null),
message.text ?? '',
];
text = parts.join(' ');
}
return Text.rich(
_getDisplayText(
text,
message.mentionedUsers,
message.attachments,
StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
fontStyle: (message.isSystem || message.isDeleted)
? FontStyle.italic
: FontStyle.normal,
),
StreamChatTheme.of(context).channelPreviewTheme.subtitle.copyWith(
fontStyle: (message.isSystem || message.isDeleted)
? FontStyle.italic
: FontStyle.normal,
fontWeight: FontWeight.bold,
),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
}
TextSpan _getDisplayText(
String text,
List<User> mentions,
List<Attachment> attachments,
TextStyle normalTextStyle,
TextStyle mentionsTextStyle) {
var textList = text.split(' ');
List<TextSpan> resList = [];
for (var e in textList) {
if (mentions != null &&
mentions.isNotEmpty &&
mentions.any((element) => '@${element.name}' == e)) {
resList.add(TextSpan(
text: '$e ',
style: mentionsTextStyle,
));
} else if (attachments != null &&
attachments.isNotEmpty &&
attachments
.where((e) => e.title != null)
.any((element) => element.title == e)) {
resList.add(TextSpan(
text: '$e ',
style: normalTextStyle.copyWith(fontStyle: FontStyle.italic),
));
} else {
resList.add(TextSpan(
text: e == textList.last ? '$e' : '$e ',
style: normalTextStyle,
));
}
}
return TextSpan(children: resList);
}
}
-357
View File
@@ -1,357 +0,0 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/message_search_item.dart';
import '../stream_chat_flutter.dart';
import 'lazy_load_scroll_view.dart';
import 'message_search_bloc.dart';
/// Callback called when tapping on a user
typedef MessageSearchItemTapCallback = void Function(GetMessageResponse);
/// Builder used to create a custom [ListUserItem] from a [User]
typedef MessageSearchItemBuilder = Widget Function(
BuildContext, GetMessageResponse);
/// Builder used when [MessageSearchListView] is empty
typedef EmptyMessageSearchBuilder = Widget Function(
BuildContext context, String searchQuery);
///
/// It shows the list of searched messages.
///
/// ```dart
/// class MessageSearchPage extends StatelessWidget {
/// @override
/// Widget build(BuildContext context) {
/// return Scaffold(
/// body: MessageSearchListView(
/// messageQuery: _channelQuery,
/// filters: {
/// 'members': {
/// r'$in': [user.id]
/// }
/// },
/// paginationParams: PaginationParams(limit: 20),
/// ),
/// );
/// }
/// }
/// ```
///
///
/// Make sure to have a [MessageSearchBloc] ancestor in order to provide the information about the messages.
/// The widget uses a [ListView.separated] to render the list of messages.
///
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme].
/// Modify it to change the widget appearance.
class MessageSearchListView extends StatefulWidget {
/// Instantiate a new MessageSearchListView
const MessageSearchListView({
Key key,
@required this.messageQuery,
@required this.filters,
this.sortOptions,
this.paginationParams,
this.emptyBuilder,
this.errorBuilder,
this.separatorBuilder,
this.itemBuilder,
this.onItemTap,
this.showResultCount = true,
}) : super(key: key);
/// Message String to search on
final String messageQuery;
/// The query filters to use.
/// You can query on any of the custom fields you've defined on the [Channel].
/// You can also filter other built-in channel fields.
final Map<String, dynamic> filters;
/// The sorting used for the channels matching the filters.
/// Sorting is based on field and direction, multiple sorting options can be provided.
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
/// Direction can be ascending or descending.
final List<SortOption> sortOptions;
/// Pagination parameters
/// limit: the number of users to return (max is 30)
/// offset: the offset (max is 1000)
/// message_limit: how many messages should be included to each channel
final PaginationParams paginationParams;
/// Builder used to create a custom item preview
final MessageSearchItemBuilder itemBuilder;
/// Function called when tapping on a [MessageSearchItem]
final MessageSearchItemTapCallback onItemTap;
/// The builder used when the channel list is empty.
final EmptyMessageSearchBuilder emptyBuilder;
/// The builder that will be used in case of error
final Widget Function(Error error) errorBuilder;
/// Builder used to create a custom item separator
final IndexedWidgetBuilder separatorBuilder;
/// Set it to false to hide total results text
final bool showResultCount;
@override
_MessageSearchListViewState createState() => _MessageSearchListViewState();
}
class _MessageSearchListViewState extends State<MessageSearchListView> {
@override
void initState() {
super.initState();
final messageSearchBloc = MessageSearchBloc.of(context);
messageSearchBloc.search(
filter: widget.filters,
sort: widget.sortOptions,
query: widget.messageQuery,
pagination: widget.paginationParams,
);
}
@override
Widget build(BuildContext context) {
final messageSearchBloc = MessageSearchBloc.of(context);
return _buildListView(messageSearchBloc);
}
Widget _separatorBuilder(BuildContext context, int index) {
return Container(
height: 1,
color: Theme.of(context).brightness == Brightness.dark
? StreamChatTheme.of(context).colorTheme.white.withOpacity(0.1)
: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.1),
);
}
Widget _listItemBuilder(
BuildContext context, GetMessageResponse getMessageResponse) {
if (widget.itemBuilder != null) {
return widget.itemBuilder(context, getMessageResponse);
}
return MessageSearchItem(
getMessageResponse: getMessageResponse,
onTap: () => widget.onItemTap(getMessageResponse),
);
}
Widget _buildQueryProgressIndicator(
context, MessageSearchBlocState messageSearchBloc) {
return StreamBuilder<bool>(
stream: messageSearchBloc.queryMessagesLoading,
initialData: false,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Container(
color: StreamChatTheme.of(context)
.colorTheme
.accentRed
.withOpacity(.2),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Center(
child: Text('Error loading messages'),
),
),
);
}
return Container(
height: 100,
padding: EdgeInsets.all(32),
child: Center(
child: snapshot.data ? CircularProgressIndicator() : Container(),
),
);
});
}
Widget _buildListView(MessageSearchBlocState messageSearchBloc) {
return StreamBuilder<List<GetMessageResponse>>(
stream: messageSearchBloc.messagesStream,
builder: (context, snapshot) {
if (snapshot.hasError) {
if (snapshot.error is Error) {
print((snapshot.error as Error).stackTrace);
}
if (widget.errorBuilder != null) {
return widget.errorBuilder(snapshot.error);
}
var message = snapshot.error.toString();
if (snapshot.error is DioError) {
final dioError = snapshot.error as DioError;
if (dioError.type == DioErrorType.RESPONSE) {
message = dioError.message;
} else {
message = 'Check your connection and retry';
}
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text.rich(
TextSpan(
children: [
WidgetSpan(
child: Padding(
padding: const EdgeInsets.only(
right: 2.0,
),
child: Icon(Icons.error_outline),
),
),
TextSpan(text: 'Error loading messages'),
],
),
style: Theme.of(context).textTheme.headline6,
),
Padding(
padding: const EdgeInsets.only(
top: 16.0,
),
child: Text(message),
),
FlatButton(
onPressed: () {
messageSearchBloc.search(
filter: widget.filters,
sort: widget.sortOptions,
query: widget.messageQuery,
pagination: widget.paginationParams,
);
},
child: Text('Retry'),
),
],
),
);
}
if (!snapshot.hasData) {
return LayoutBuilder(
builder: (context, viewportConstraints) {
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: Center(
child: CircularProgressIndicator(),
),
),
);
},
);
}
final items = snapshot.data;
if (items.isEmpty) {
if (widget.emptyBuilder != null) {
return widget.emptyBuilder(context, widget.messageQuery);
}
return LayoutBuilder(
builder: (context, viewportConstraints) {
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: Center(
child: Text('There are no messages currently'),
),
),
);
},
);
}
Widget child;
child = LazyLoadScrollView(
onEndOfPage: () => messageSearchBloc.search(
filter: widget.filters,
sort: widget.sortOptions,
pagination: widget.paginationParams.copyWith(
offset: messageSearchBloc.messageResponses?.length ?? 0,
),
query: widget.messageQuery,
),
child: ListView.separated(
physics: AlwaysScrollableScrollPhysics(),
itemCount: items.isNotEmpty ? items.length + 1 : items.length,
separatorBuilder: (_, index) {
if (widget.separatorBuilder != null) {
return widget.separatorBuilder(context, index);
}
return _separatorBuilder(context, index);
},
itemBuilder: (context, index) {
if (index < items.length) {
return _listItemBuilder(context, items[index]);
}
return _buildQueryProgressIndicator(context, messageSearchBloc);
},
),
);
if (widget.showResultCount) {
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(
'${items.length} results',
style: TextStyle(
color: StreamChatTheme.of(context).colorTheme.grey,
),
),
),
),
Expanded(child: child),
],
);
}
return child;
},
);
}
@override
void didUpdateWidget(MessageSearchListView oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.filters?.toString() != oldWidget.filters?.toString() ||
jsonEncode(widget.sortOptions) != jsonEncode(oldWidget.sortOptions) ||
widget.paginationParams?.toJson()?.toString() !=
oldWidget.paginationParams?.toJson()?.toString() ||
widget.messageQuery?.toString() != oldWidget.messageQuery?.toString()) {
final messageSearchBloc = MessageSearchBloc.of(context);
messageSearchBloc.search(
filter: widget.filters,
sort: widget.sortOptions,
query: widget.messageQuery,
pagination: widget.paginationParams,
);
}
}
}
-75
View File
@@ -1,75 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:stream_chat/stream_chat.dart';
import 'stream_chat_theme.dart';
import 'utils.dart';
class MessageText extends StatelessWidget {
const MessageText({
Key key,
@required this.message,
@required this.messageTheme,
this.onMentionTap,
this.onLinkTap,
}) : super(key: key);
final Message message;
final void Function(User) onMentionTap;
final void Function(String) onLinkTap;
final MessageTheme messageTheme;
@override
Widget build(BuildContext context) {
final text = _replaceMentions(message.text);
return MarkdownBody(
data: text,
onTapLink: (
String link,
String href,
String title,
) {
if (link.startsWith('@')) {
final mentionedUser = message.mentionedUsers.firstWhere(
(u) => '@${u.name}' == link,
orElse: () => null,
);
if (onMentionTap != null) {
onMentionTap(mentionedUser);
} else {
print('tap on ${mentionedUser.name}');
}
} else {
if (onLinkTap != null) {
onLinkTap(link);
} else {
launchURL(context, link);
}
}
},
styleSheet: MarkdownStyleSheet.fromTheme(
Theme.of(context).copyWith(
textTheme: Theme.of(context).textTheme.apply(
bodyColor: messageTheme.messageText.color,
decoration: messageTheme.messageText.decoration,
decorationColor: messageTheme.messageText.decorationColor,
decorationStyle: messageTheme.messageText.decorationStyle,
fontFamily: messageTheme.messageText.fontFamily,
),
),
).copyWith(
a: messageTheme.messageLinks,
p: messageTheme.messageText,
),
);
}
String _replaceMentions(String text) {
message.mentionedUsers?.forEach((u) {
text = text.replaceAll(
'@${u.name}', '[@${u.name}](@${u.name.replaceAll(' ', '')})');
});
return text;
}
}
File diff suppressed because it is too large Load Diff
-81
View File
@@ -1,81 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
class OptionListTile extends StatelessWidget {
final String title;
final Widget leading;
final Widget trailing;
final VoidCallback onTap;
final Color titleColor;
final Color tileColor;
final Color separatorColor;
final TextStyle titleTextStyle;
OptionListTile({
this.title,
this.leading,
this.trailing,
this.onTap,
this.titleColor,
this.tileColor,
this.separatorColor,
this.titleTextStyle,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
color: separatorColor ??
StreamChatTheme.of(context).colorTheme.greyGainsboro,
height: 1.0,
),
Material(
color: tileColor ?? StreamChatTheme.of(context).colorTheme.white,
child: Container(
height: 63.0,
child: InkWell(
onTap: onTap,
child: Row(
children: [
if (leading != null) Center(child: leading),
if (leading == null)
SizedBox(
width: 16.0,
),
Expanded(
flex: 4,
child: Text(
title,
style: titleTextStyle ??
(titleColor == null
? StreamChatTheme.of(context).textTheme.bodyBold
: StreamChatTheme.of(context)
.textTheme
.bodyBold
.copyWith(
color: titleColor,
)),
),
),
Expanded(
flex: 2,
child: Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Align(
alignment: Alignment.centerRight,
child: trailing ?? Container(),
),
),
),
],
),
),
),
),
],
);
}
}
-321
View File
@@ -1,321 +0,0 @@
import 'dart:math';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:emojis/emoji.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:video_player/video_player.dart';
import 'attachment_error.dart';
import 'image_attachment.dart';
import 'message_text.dart';
import 'stream_chat_theme.dart';
import 'user_avatar.dart';
import 'utils.dart';
import 'extension.dart';
typedef QuotedMessageAttachmentThumbnailBuilder = Widget Function(
BuildContext,
Attachment,
);
class _VideoAttachmentThumbnail extends StatefulWidget {
final Size size;
final Attachment attachment;
const _VideoAttachmentThumbnail({
Key key,
@required this.attachment,
this.size = const Size(32, 32),
}) : super(key: key);
@override
_VideoAttachmentThumbnailState createState() =>
_VideoAttachmentThumbnailState();
}
class _VideoAttachmentThumbnailState extends State<_VideoAttachmentThumbnail> {
VideoPlayerController _controller;
@override
void initState() {
super.initState();
_controller = VideoPlayerController.network(widget.attachment.assetUrl)
..initialize().then((_) {
setState(() {}); //when your thumbnail will show.
});
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
height: widget.size.height,
width: widget.size.width,
child: _controller.value.initialized
? VideoPlayer(_controller)
: CircularProgressIndicator());
}
}
///
class QuotedMessageWidget extends StatelessWidget {
/// The message
final Message message;
/// The message theme
final MessageTheme messageTheme;
/// If true the widget will be mirrored
final bool reverse;
/// If true the message will show a grey border
final bool showBorder;
/// limit of the text message shown
final int textLimit;
/// Map that defines a thumbnail builder for an attachment type
final Map<String, QuotedMessageAttachmentThumbnailBuilder>
attachmentThumbnailBuilders;
final GestureTapCallback onTap;
///
QuotedMessageWidget({
Key key,
@required this.message,
@required this.messageTheme,
this.reverse = false,
this.showBorder = false,
this.textLimit = 170,
this.attachmentThumbnailBuilders,
this.onTap,
}) : super(key: key);
bool get _hasAttachments => message.attachments?.isNotEmpty == true;
bool get _containsScrapeUrl =>
message.attachments?.any((element) => element.ogScrapeUrl != null) ==
true;
bool get _containsText => message?.text?.isNotEmpty == true;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.only(top: 8, bottom: 6, right: 4, left: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(child: _buildMessage(context)),
SizedBox(width: 4),
_buildUserAvatar(),
],
),
),
);
}
Widget _buildMessage(BuildContext context) {
final isOnlyEmoji =
message.text.characters.every((c) => Emoji.byChar(c) != null);
var msg = _hasAttachments && !_containsText
? message.copyWith(text: message.attachments.last?.title ?? '')
: message;
if (msg.text.length > textLimit) {
msg = msg.copyWith(text: '${msg.text.substring(0, textLimit - 3)}...');
}
final children = [
if (_hasAttachments) _parseAttachments(context),
if (msg.text.isNotEmpty)
Flexible(
child: Transform(
transform: Matrix4.rotationY(reverse ? pi : 0),
alignment: Alignment.center,
child: MessageText(
message: msg,
messageTheme: isOnlyEmoji && _containsText
? messageTheme.copyWith(
messageText: messageTheme.messageText.copyWith(
fontSize: 24,
))
: messageTheme,
),
),
),
].insertBetween(const SizedBox(width: 8));
return Container(
decoration: BoxDecoration(
color: _getBackgroundColor(context),
border: showBorder
? Border.all(
color: StreamChatTheme.of(context).colorTheme.greyGainsboro,
)
: null,
borderRadius: BorderRadius.only(
topRight: Radius.circular(12),
topLeft: Radius.circular(12),
bottomLeft: Radius.circular(12),
),
),
padding: const EdgeInsets.all(8),
child: Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment:
reverse ? MainAxisAlignment.end : MainAxisAlignment.start,
children: reverse ? children.reversed.toList() : children,
),
);
}
Widget _buildUrlAttachment(Attachment attachment) {
final size = Size(32, 32);
if (attachment.thumbUrl != null) {
return Container(
height: size.height,
width: size.width,
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
image: CachedNetworkImageProvider(
attachment.imageUrl,
),
),
),
);
}
return AttachmentError(
attachment: attachment,
size: size,
);
}
Widget _parseAttachments(BuildContext context) {
Widget child;
Attachment attachment;
if (_containsScrapeUrl) {
attachment = message.attachments.firstWhere(
(element) => element.ogScrapeUrl != null,
);
child = _buildUrlAttachment(attachment);
} else {
QuotedMessageAttachmentThumbnailBuilder attachmentBuilder;
attachment = message.attachments.last;
if (attachmentThumbnailBuilders?.containsKey(attachment?.type) == true) {
attachmentBuilder = attachmentThumbnailBuilders[attachment?.type];
}
attachmentBuilder = _defaultAttachmentBuilder[attachment?.type];
if (attachmentBuilder == null) {
child = Offstage();
}
child = attachmentBuilder(context, attachment);
}
child = AbsorbPointer(child: child);
return Transform(
transform: Matrix4.rotationY(reverse ? pi : 0),
alignment: Alignment.center,
child: Material(
clipBehavior: Clip.hardEdge,
color: Colors.transparent,
shape: attachment.type == 'file' ? null : _getDefaultShape(context),
child: child,
),
);
}
ShapeBorder _getDefaultShape(BuildContext context) {
return RoundedRectangleBorder(
side: BorderSide(
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
),
borderRadius: BorderRadius.circular(8),
);
}
Widget _buildUserAvatar() {
return Transform(
transform: Matrix4.rotationY(reverse ? pi : 0),
alignment: Alignment.center,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: UserAvatar(
user: message.user,
constraints: BoxConstraints.tightFor(
height: 24,
width: 24,
),
showOnlineStatus: false,
),
),
);
}
Map<String, QuotedMessageAttachmentThumbnailBuilder>
get _defaultAttachmentBuilder {
return {
'image': (_, attachment) {
return ImageAttachment(
attachment: attachment,
message: message,
messageTheme: messageTheme,
size: Size(32, 32),
);
},
'video': (_, attachment) {
return _VideoAttachmentThumbnail(
key: ValueKey(attachment.assetUrl),
attachment: attachment,
);
},
'giphy': (_, attachment) {
final size = Size(32, 32);
return CachedNetworkImage(
height: size?.height,
width: size?.width,
placeholder: (_, __) {
return Container(
width: size?.width,
height: size?.height,
child: Center(
child: CircularProgressIndicator(),
),
);
},
imageUrl:
attachment.thumbUrl ?? attachment.imageUrl ?? attachment.assetUrl,
errorWidget: (context, url, error) => AttachmentError(
attachment: attachment,
size: size,
),
fit: BoxFit.cover,
);
},
'file': (_, attachment) {
return Container(
height: 32,
width: 32,
child: getFileTypeImage(attachment.extraData['mime_type']),
);
},
};
}
Color _getBackgroundColor(BuildContext context) {
if (_containsScrapeUrl) {
return StreamChatTheme.of(context).colorTheme.blueAlice;
}
return messageTheme.messageBackgroundColor;
}
}
-244
View File
@@ -1,244 +0,0 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:stream_chat_flutter/src/reaction_icon.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class ReactionBubble extends StatelessWidget {
const ReactionBubble({
Key key,
@required this.reactions,
@required this.borderColor,
@required this.backgroundColor,
this.reverse = false,
this.flipTail = false,
this.highlightOwnReactions = true,
}) : super(key: key);
final List<Reaction> reactions;
final Color borderColor;
final Color backgroundColor;
final bool reverse;
final bool flipTail;
final bool highlightOwnReactions;
@override
Widget build(BuildContext context) {
final reactionIcons = StreamChatTheme.of(context).reactionIcons;
final offset = reactions.length > 1 ? 16.0 : 2.0;
return Transform(
transform: Matrix4.rotationY(reverse ? pi : 0),
alignment: Alignment.center,
child: Stack(
alignment: Alignment.center,
children: [
Transform.translate(
offset: Offset(reverse ? offset : -offset, 0),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 4),
decoration: BoxDecoration(
border: Border.all(
color: borderColor,
),
color: backgroundColor,
borderRadius: BorderRadius.all(Radius.circular(14)),
),
child: LayoutBuilder(
builder: (context, constraints) {
return Flex(
direction: Axis.horizontal,
mainAxisSize: MainAxisSize.min,
children: [
if (constraints.maxWidth < double.infinity)
...reactions
.take((constraints.maxWidth) ~/ 22)
.map((reaction) {
return _buildReaction(
reactionIcons,
reaction,
context,
);
}).toList(),
if (constraints.maxWidth == double.infinity)
...reactions.map((reaction) {
return _buildReaction(
reactionIcons,
reaction,
context,
);
}).toList(),
],
);
},
),
),
),
Positioned(
bottom: 0,
left: reverse ? null : 11,
right: !reverse ? null : 11,
child: _buildReactionsTail(context),
),
],
),
);
}
Widget _buildReaction(
List<ReactionIcon> reactionIcons,
Reaction reaction,
BuildContext context,
) {
final reactionIcon = reactionIcons.firstWhere(
(r) => r.type == reaction.type,
orElse: () => null,
);
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 4.0,
),
child: reactionIcon != null
? StreamSvgIcon(
assetName: reactionIcon.assetName,
width: 16,
height: 16,
color: (!highlightOwnReactions ||
reaction.user.id == StreamChat.of(context).user.id)
? StreamChatTheme.of(context).colorTheme.accentBlue
: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5),
)
: Icon(
Icons.help_outline_rounded,
size: 16,
color: (!highlightOwnReactions ||
reaction.user.id == StreamChat.of(context).user.id)
? StreamChatTheme.of(context).colorTheme.accentBlue
: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(.5),
),
);
}
Widget _buildReactionsTail(BuildContext context) {
final tail = CustomPaint(
painter: ReactionBubblePainter(
backgroundColor,
borderColor,
),
);
if (!flipTail) {
return tail;
} else {
return Transform(
transform: Matrix4.rotationY(pi),
alignment: Alignment.center,
child: tail,
);
}
}
}
class ReactionBubblePainter extends CustomPainter {
final Color color;
final Color borderColor;
ReactionBubblePainter(
this.color,
this.borderColor,
);
@override
void paint(Canvas canvas, Size size) {
_drawOval(size, canvas);
_drawOvalBorder(size, canvas);
_drawArc(size, canvas);
_drawBorder(size, canvas);
}
void _drawOvalBorder(Size size, Canvas canvas) {
final paint = Paint()
..color = borderColor
..strokeWidth = 1
..style = PaintingStyle.stroke;
final path = Path();
path.addOval(
Rect.fromCircle(
center: Offset(4, 3),
radius: 2,
),
);
canvas.drawPath(path, paint);
}
void _drawOval(Size size, Canvas canvas) {
final paint = Paint()
..color = color
..strokeWidth = 1;
final path = Path();
path.addOval(Rect.fromCircle(
center: Offset(4, 3),
radius: 2,
));
canvas.drawPath(path, paint);
}
void _drawBorder(Size size, Canvas canvas) {
final paint = Paint()
..color = borderColor
..strokeWidth = 1
..style = PaintingStyle.stroke;
final dy = -2.2;
final startAngle = 1.1;
final sweepAngle = 1.2;
final path = Path();
path.addArc(
Rect.fromCircle(
center: Offset(1, dy),
radius: 4,
),
-pi * startAngle,
-pi / sweepAngle,
);
canvas.drawPath(path, paint);
}
void _drawArc(Size size, Canvas canvas) {
final paint = Paint()
..color = color
..strokeWidth = 1;
final dy = -2.2;
final startAngle = 1;
final sweepAngle = 1.3;
final path = Path();
path.addArc(
Rect.fromCircle(
center: Offset(1, dy),
radius: 4,
),
-pi * startAngle,
-pi * sweepAngle,
);
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
-9
View File
@@ -1,9 +0,0 @@
class ReactionIcon {
final String type;
final String assetName;
ReactionIcon({
this.type,
this.assetName,
});
}
-160
View File
@@ -1,160 +0,0 @@
import 'package:ezanimation/ezanimation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import '../stream_chat_flutter.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/reaction_picker.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/reaction_picker_paint.png)
///
/// It shows a reaction picker
///
/// Usually you don't use this widget as it's one of the default widgets used by [MessageWidget.onMessageActions].
class ReactionPicker extends StatefulWidget {
const ReactionPicker({
Key key,
@required this.message,
@required this.messageTheme,
}) : super(key: key);
final Message message;
final MessageTheme messageTheme;
@override
_ReactionPickerState createState() => _ReactionPickerState();
}
class _ReactionPickerState extends State<ReactionPicker>
with TickerProviderStateMixin {
List<EzAnimation> animations = [];
@override
Widget build(BuildContext context) {
final reactionIcons = StreamChatTheme.of(context).reactionIcons;
if (animations.isEmpty && reactionIcons.isNotEmpty) {
reactionIcons.forEach((element) {
animations.add(
EzAnimation.sequence(
[
SequenceItem(0.0, 1.4),
SequenceItem(1.4, 1.0),
],
Duration(milliseconds: 500),
vsync: this,
),
);
});
triggerAnimations();
}
return TweenAnimationBuilder<double>(
tween: Tween(begin: 0.0, end: 1.0),
curve: Curves.easeInOutExpo,
duration: Duration(milliseconds: 500),
builder: (context, val, wid) {
return Transform.scale(
scale: val,
child: Material(
color: widget.messageTheme.reactionsBackgroundColor,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: reactionIcons.map((reactionIcon) {
final ownReactionIndex = widget.message.ownReactions
?.indexWhere((reaction) =>
reaction.type == reactionIcon.type) ??
-1;
var index = reactionIcons.indexOf(reactionIcon);
return IconButton(
iconSize: 24,
icon: AnimatedBuilder(
animation: animations[index],
builder: (context, val) {
return Transform(
transform: Matrix4.identity()
..scale(animations[index].value,
animations[index].value)
..rotateZ(1.0 - animations[index].value),
child: StreamSvgIcon(
assetName: reactionIcon.assetName,
height: animations[index].value * 24.0,
width: animations[index].value * 24.0,
color: ownReactionIndex != -1
? StreamChatTheme.of(context)
.colorTheme
.accentBlue
: Theme.of(context)
.iconTheme
.color
.withOpacity(.5),
),
);
}),
onPressed: () {
if (ownReactionIndex != -1) {
removeReaction(
context,
widget.message.ownReactions[ownReactionIndex],
);
} else {
sendReaction(
context,
reactionIcon.type,
);
}
},
);
}).toList(),
),
),
);
});
}
void triggerAnimations() async {
for (var a in animations) {
a.start();
await Future.delayed(Duration(milliseconds: 100));
}
}
void pop() async {
for (var a in animations) {
a.stop();
}
Navigator.of(context).pop();
}
/// Add a reaction to the message
void sendReaction(BuildContext context, String reactionType) {
StreamChannel.of(context).channel.sendReaction(
widget.message,
reactionType,
enforceUnique: true,
);
pop();
}
/// Remove a reaction from the message
void removeReaction(BuildContext context, Reaction reaction) {
StreamChannel.of(context).channel.deleteReaction(widget.message, reaction);
pop();
}
@override
void dispose() {
for (var a in animations) {
a?.dispose();
}
super.dispose();
}
}
-40
View File
@@ -1,40 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Used to show the sending status of the message
class SendingIndicator extends StatelessWidget {
final Message message;
final bool isMessageRead;
final double size;
const SendingIndicator({
Key key,
this.message,
this.isMessageRead = false,
this.size = 12,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (isMessageRead) {
return StreamSvgIcon.checkAll(
size: size,
color: StreamChatTheme.of(context).colorTheme.accentBlue,
);
}
if (message.status == MessageSendingStatus.SENT || message.status == null) {
return StreamSvgIcon.check(
size: size,
color: IconTheme.of(context).color.withOpacity(0.5),
);
}
if (message.status == MessageSendingStatus.SENDING ||
message.status == MessageSendingStatus.UPDATING) {
return Icon(
Icons.access_time,
size: size,
);
}
return SizedBox();
}
}
-356
View File
@@ -1,356 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/stream_chat.dart';
enum QueryDirection { top, bottom }
/// Widget used to provide information about the channel to the widget tree
///
/// Use [StreamChannel.of] to get the current [StreamChannelState] instance.
class StreamChannel extends StatefulWidget {
const StreamChannel({
Key key,
@required this.child,
@required this.channel,
this.showLoading = true,
this.initialMessageId,
}) : assert(child != null),
assert(channel != null),
super(key: key);
final Widget child;
final Channel channel;
final bool showLoading;
/// If passed the channel will load from this particular message.
final String initialMessageId;
/// Use this method to get the current [StreamChannelState] instance
static StreamChannelState of(BuildContext context) {
StreamChannelState streamChannelState;
streamChannelState = context.findAncestorStateOfType<StreamChannelState>();
if (streamChannelState == null) {
throw Exception(
'You must have a StreamChannel widget at the top of your widget tree');
}
return streamChannelState;
}
@override
StreamChannelState createState() => StreamChannelState();
}
class StreamChannelState extends State<StreamChannel> {
/// Current channel
Channel get channel => widget.channel;
/// InitialMessageId
String get initialMessageId => widget.initialMessageId;
/// Current channel state stream
Stream<ChannelState> get channelStateStream =>
widget.channel.state.channelStateStream;
final _queryTopMessagesController = BehaviorSubject.seeded(false);
final _queryBottomMessagesController = BehaviorSubject.seeded(false);
/// The stream notifying the state of [_queryTopMessages] call
Stream<bool> get queryTopMessages => _queryTopMessagesController.stream;
/// The stream notifying the state of [_queryBottomMessages] call
Stream<bool> get queryBottomMessages => _queryBottomMessagesController.stream;
bool _topPaginationEnded = false;
bool _bottomPaginationEnded = false;
Future<void> _queryTopMessages({
int limit = 20,
bool preferOffline = false,
}) async {
if (_topPaginationEnded || _queryTopMessagesController?.value == true) {
return;
}
_queryTopMessagesController.add(true);
if (channel.state.messages.isEmpty) {
return _queryTopMessagesController.add(false);
}
final oldestMessage = channel.state.messages.first;
try {
final state = await queryBeforeMessage(
oldestMessage.id,
limit: limit,
preferOffline: preferOffline,
);
if (state.messages.isEmpty || state.messages.length < limit) {
_topPaginationEnded = true;
}
_queryTopMessagesController.add(false);
} catch (e, stk) {
_queryTopMessagesController.addError(e, stk);
}
}
Future<void> _queryBottomMessages({
int limit = 20,
bool preferOffline = false,
}) async {
if (_bottomPaginationEnded ||
_queryBottomMessagesController?.value == true ||
channel?.state?.isUpToDate == true) return;
_queryBottomMessagesController.add(true);
if (channel.state.messages.isEmpty) {
return _queryBottomMessagesController.add(false);
}
final recentMessage = channel.state.messages.last;
try {
final state = await queryAfterMessage(
recentMessage.id,
limit: limit,
preferOffline: preferOffline,
);
if (state.messages.isEmpty || state.messages.length < limit) {
_bottomPaginationEnded = true;
}
_queryBottomMessagesController.add(false);
} catch (e, stk) {
_queryBottomMessagesController.addError(e, stk);
}
}
/// Calls [channel.query] updating [queryMessage] stream
Future<void> queryMessages({QueryDirection direction = QueryDirection.top}) {
if (direction == QueryDirection.top) return _queryTopMessages();
return _queryBottomMessages();
}
/// Calls [channel.getReplies] updating [queryMessage] stream
Future<void> getReplies(
String parentId, {
int limit = 50,
bool preferOffline = false,
}) async {
if (_topPaginationEnded || _queryTopMessagesController.value) return;
_queryTopMessagesController.add(true);
Message message;
if (channel.state.threads.containsKey(parentId)) {
final thread = channel.state.threads[parentId];
if (thread.isNotEmpty) {
message = thread.first;
}
}
try {
final response = await channel.getReplies(
parentId,
PaginationParams(
lessThan: message?.id,
limit: limit,
),
preferOffline: preferOffline,
);
if (response.messages.isEmpty || response.messages.length < limit) {
_topPaginationEnded = true;
}
_queryTopMessagesController.add(false);
} catch (e, stk) {
_queryTopMessagesController.addError(e, stk);
}
}
/// Query the channel members and watchers
Future<void> queryMembersAndWatchers() async {
await widget.channel.query(
membersPagination: PaginationParams(
offset: channel.state.members?.length,
limit: 100,
),
watchersPagination: PaginationParams(
offset: channel.state.watchers?.length,
limit: 100,
),
);
}
/// Loads channel at specific message
Future<void> loadChannelAtMessage(
String messageId, {
int before = 20,
int after = 20,
bool preferOffline = false,
}) {
return queryAtMessage(
messageId: messageId,
before: before,
after: after,
preferOffline: preferOffline,
);
}
///
Future<void> queryAtMessage({
String messageId,
int before = 20,
int after = 20,
bool preferOffline = false,
}) async {
if (channel.state == null) return;
channel.state.isUpToDate = false;
channel.state.truncate();
if (messageId == null) {
await channel.query(
messagesPagination: PaginationParams(
limit: before,
),
preferOffline: preferOffline,
);
channel.state.isUpToDate = true;
return;
}
return Future.wait([
queryBeforeMessage(
messageId,
limit: before,
preferOffline: preferOffline,
),
queryAfterMessage(
messageId,
limit: after,
preferOffline: preferOffline,
),
]);
}
///
Future<ChannelState> queryBeforeMessage(
String messageId, {
int limit = 20,
bool preferOffline = false,
}) {
return channel.query(
messagesPagination: PaginationParams(
lessThan: messageId,
limit: limit,
),
preferOffline: preferOffline,
);
}
///
Future<ChannelState> queryAfterMessage(
String messageId, {
int limit = 20,
bool preferOffline = false,
}) async {
final state = await channel.query(
messagesPagination: PaginationParams(
greaterThanOrEqual: messageId,
limit: limit,
),
preferOffline: preferOffline,
);
if (state.messages.isEmpty || state.messages.length < limit) {
channel.state.isUpToDate = true;
}
return state;
}
///
Future<Message> getMessage(String messageId) async {
var message = channel.state.messages.firstWhere(
(it) => it.id == messageId,
orElse: () => null,
);
if (message == null) {
final response = await channel.getMessagesById([messageId]);
message = response.messages.first;
}
return message;
}
/// Reloads the channel with latest message
Future<void> reloadChannel() => queryAtMessage(before: 30);
List<Future<bool>> _futures;
Future<bool> get _loadChannelAtMessage async {
try {
await loadChannelAtMessage(initialMessageId);
return true;
} catch (e, stk) {
print('Error: $e\nStack: $stk');
rethrow;
}
}
@override
void initState() {
super.initState();
_futures = [widget.channel.initialized];
if (initialMessageId != null) {
_futures.add(_loadChannelAtMessage);
}
}
@override
void dispose() {
_queryTopMessagesController.close();
_queryBottomMessagesController.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
Widget child = FutureBuilder<List<bool>>(
future: Future.wait(_futures),
initialData: [
channel.state != null,
if (initialMessageId != null) false,
],
builder: (context, snapshot) {
if (snapshot.hasError) {
if (snapshot.error is Error) {
print((snapshot.error as Error).stackTrace);
}
var message = snapshot.error.toString();
if (snapshot.error is DioError) {
final dioError = snapshot.error as DioError;
if (dioError.type == DioErrorType.RESPONSE) {
message = dioError.message;
} else {
message = 'Check your connection and retry';
}
}
return Center(
child: Text(message),
);
}
final initialized = snapshot.data[0];
final dataLoaded = initialMessageId == null ? true : snapshot.data[1];
if (widget.showLoading && (!initialized || !dataLoaded)) {
return Center(
child: CircularProgressIndicator(),
);
}
return widget.child;
},
);
if (initialMessageId != null) {
child = Material(child: child);
}
return child;
}
}
-179
View File
@@ -1,179 +0,0 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app_badger/flutter_app_badger.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
/// Widget used to provide information about the chat to the widget tree
///
/// class MyApp extends StatelessWidget {
/// final Client client;
///
/// MyApp(this.client);
///
/// @override
/// Widget build(BuildContext context) {
/// return MaterialApp(
/// home: Container(
/// child: StreamChat(
/// client: client,
/// child: ChannelListPage(),
/// ),
/// ),
/// );
/// }
/// }
///
/// Use [StreamChat.of] to get the current [StreamChatState] instance.
class StreamChat extends StatefulWidget {
final Client client;
final Widget child;
final StreamChatThemeData streamChatThemeData;
StreamChat({
Key key,
@required this.client,
@required this.child,
this.streamChatThemeData,
}) : super(
key: key,
);
@override
StreamChatState createState() => StreamChatState();
/// Use this method to get the current [StreamChatState] instance
static StreamChatState of(BuildContext context) {
StreamChatState streamChatState;
streamChatState = context.findAncestorStateOfType<StreamChatState>();
if (streamChatState == null) {
throw Exception(
'You must have a StreamChat widget at the top of your widget tree');
}
return streamChatState;
}
}
/// The current state of the StreamChat widget
class StreamChatState extends State<StreamChat> with WidgetsBindingObserver {
Client get client => widget.client;
Timer _disconnectTimer;
@override
Widget build(BuildContext context) {
final theme = _getTheme(context, widget.streamChatThemeData);
return StreamChatTheme(
data: theme,
child: Builder(
builder: (context) {
final materialTheme = Theme.of(context);
final streamTheme = StreamChatTheme.of(context);
return Theme(
data: materialTheme.copyWith(
primaryIconTheme: streamTheme.primaryIconTheme,
accentColor: streamTheme.colorTheme.accentBlue,
scaffoldBackgroundColor: streamTheme.colorTheme.white,
buttonTheme: streamTheme.buttonTheme,
),
child: widget.child,
);
},
),
);
}
StreamChatThemeData _getTheme(
BuildContext context,
StreamChatThemeData themeData,
) {
final defaultTheme = StreamChatThemeData.getDefaultTheme(Theme.of(context));
return defaultTheme.merge(themeData) ?? themeData;
}
/// The current user
User get user => widget.client.state.user;
/// The current user as a stream
Stream<User> get userStream => widget.client.state.userStream;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
client.state?.totalUnreadCountStream?.listen((count) {
if (count > 0) {
FlutterAppBadger.updateBadgeCount(count);
} else {
FlutterAppBadger.removeBadge();
}
});
}
StreamSubscription _newMessageSubscription;
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (client.state?.user != null) {
if (state == AppLifecycleState.paused) {
if (client.showLocalNotification != null) {
_newMessageSubscription = client
.on(EventType.messageNew)
.where((e) => e.user?.id != user.id)
.where((e) => e.message.silent != true)
.where((e) => e.message.shadowed != true)
.listen((event) async {
final channel = client.channel(
event.channelType,
id: event.channelId,
);
client.showLocalNotification(
event.message,
ChannelModel(
id: channel.id,
createdAt: channel.createdAt,
extraData: channel.extraData,
type: channel.type,
memberCount: channel.memberCount,
frozen: channel.frozen,
cid: channel.cid,
deletedAt: channel.deletedAt,
config: channel.config,
createdBy: channel.createdBy,
updatedAt: channel.updatedAt,
lastMessageAt: channel.lastMessageAt,
),
);
});
_disconnectTimer = Timer(client.backgroundKeepAlive, () {
client.disconnect();
});
} else {
client.disconnect();
}
} else if (state == AppLifecycleState.resumed) {
_newMessageSubscription?.cancel();
if (_disconnectTimer?.isActive == true) {
_disconnectTimer.cancel();
} else {
if (client.wsConnectionStatus.value ==
ConnectionStatus.disconnected) {
NotificationService.handleIosMessageQueue(client);
client.connect();
}
}
}
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
}
-908
View File
@@ -1,908 +0,0 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/channel_header.dart';
import 'package:stream_chat_flutter/src/channel_preview.dart';
import 'package:stream_chat_flutter/src/message_input.dart';
import 'package:stream_chat_flutter/src/reaction_icon.dart';
import 'package:stream_chat_flutter/src/utils.dart';
/// Inherited widget providing the [StreamChatThemeData] to the widget tree
class StreamChatTheme extends InheritedWidget {
final StreamChatThemeData data;
StreamChatTheme({
Key key,
@required this.data,
Widget child,
}) : super(
key: key,
child: child,
);
@override
bool updateShouldNotify(StreamChatTheme old) {
return data != old.data;
}
/// Use this method to get the current [StreamChatThemeData] instance
static StreamChatThemeData of(BuildContext context) {
final streamChatTheme =
context.dependOnInheritedWidgetOfExactType<StreamChatTheme>();
if (streamChatTheme == null) {
throw Exception(
'You must have a StreamChatTheme widget at the top of your widget tree',
);
}
return streamChatTheme.data;
}
}
/// Theme data
class StreamChatThemeData {
/// The text themes used in the widgets
final TextTheme textTheme;
/// The button themes used in the widgets
final ButtonThemeData buttonTheme;
/// The text themes used in the widgets
final ColorTheme colorTheme;
/// Theme of the [ChannelPreview]
final ChannelPreviewTheme channelPreviewTheme;
/// Theme of the chat widgets dedicated to a channel
final ChannelTheme channelTheme;
/// Theme of the current user messages
final MessageTheme ownMessageTheme;
/// Theme of other users messages
final MessageTheme otherMessageTheme;
/// The widget that will be built when the channel image is unavailable
final Widget Function(BuildContext, Channel) defaultChannelImage;
/// The widget that will be built when the user image is unavailable
final Widget Function(BuildContext, User) defaultUserImage;
/// Primary icon theme
final IconThemeData primaryIconTheme;
/// Assets used for rendering reactions
final List<ReactionIcon> reactionIcons;
/// Create a theme from scratch
const StreamChatThemeData({
this.textTheme,
this.buttonTheme,
this.colorTheme,
this.channelPreviewTheme,
this.channelTheme,
this.otherMessageTheme,
this.ownMessageTheme,
this.defaultChannelImage,
this.defaultUserImage,
this.primaryIconTheme,
this.reactionIcons,
});
/// Create a theme from a Material [Theme]
factory StreamChatThemeData.fromTheme(ThemeData theme) {
final defaultTheme = getDefaultTheme(theme);
final customizedTheme = StreamChatThemeData(
primaryIconTheme: theme.primaryIconTheme,
ownMessageTheme: MessageTheme(
replies: TextStyle(color: theme.accentColor),
messageLinks: TextStyle(color: theme.accentColor),
),
otherMessageTheme: MessageTheme(
replies: TextStyle(color: theme.accentColor),
messageLinks: TextStyle(color: theme.accentColor),
),
);
return defaultTheme.merge(customizedTheme) ?? customizedTheme;
}
/// Creates a copy of [StreamChatThemeData] with specified attributes overridden.
StreamChatThemeData copyWith({
TextTheme textTheme,
ButtonThemeData buttonTheme,
ColorTheme colorTheme,
ChannelPreviewTheme channelPreviewTheme,
ChannelTheme channelTheme,
MessageTheme ownMessageTheme,
MessageTheme otherMessageTheme,
Widget Function(BuildContext, Channel) defaultChannelImage,
Widget Function(BuildContext, User) defaultUserImage,
IconThemeData primaryIconTheme,
List<ReactionIcon> reactionIcons,
}) =>
StreamChatThemeData(
textTheme: textTheme ?? this.textTheme,
buttonTheme: buttonTheme ?? this.buttonTheme,
colorTheme: colorTheme ?? this.colorTheme,
primaryIconTheme: primaryIconTheme ?? this.primaryIconTheme,
defaultChannelImage: defaultChannelImage ?? this.defaultChannelImage,
defaultUserImage: defaultUserImage ?? this.defaultUserImage,
channelPreviewTheme: channelPreviewTheme ?? this.channelPreviewTheme,
channelTheme: channelTheme ?? this.channelTheme,
ownMessageTheme: ownMessageTheme ?? this.ownMessageTheme,
otherMessageTheme: otherMessageTheme ?? this.otherMessageTheme,
reactionIcons: reactionIcons ?? this.reactionIcons,
);
StreamChatThemeData merge(StreamChatThemeData other) {
if (other == null) return this;
return copyWith(
textTheme: textTheme?.merge(other.textTheme) ?? other.textTheme,
buttonTheme: other.buttonTheme,
colorTheme: colorTheme?.merge(other.colorTheme) ?? other.colorTheme,
primaryIconTheme: other.primaryIconTheme,
defaultChannelImage: other.defaultChannelImage,
defaultUserImage: other.defaultUserImage,
channelPreviewTheme:
channelPreviewTheme?.merge(other.channelPreviewTheme) ??
other.channelPreviewTheme,
channelTheme:
channelTheme?.merge(other.channelTheme) ?? other.channelTheme,
ownMessageTheme: ownMessageTheme?.merge(other.ownMessageTheme) ??
other.ownMessageTheme,
otherMessageTheme: otherMessageTheme?.merge(other.otherMessageTheme) ??
other.otherMessageTheme,
reactionIcons: other.reactionIcons,
);
}
/// Get the default Stream Chat theme
static StreamChatThemeData getDefaultTheme(ThemeData theme) {
final accentColor = Color(0xff006cff);
final isDark = theme.brightness == Brightness.dark;
final textTheme = isDark ? TextTheme.dark() : TextTheme.light();
final colorTheme = isDark ? ColorTheme.dark() : ColorTheme.light();
return StreamChatThemeData(
textTheme: textTheme,
colorTheme: colorTheme,
buttonTheme: ButtonThemeData(
height: 48.0,
buttonColor: isDark ? Color(0xffffffff) : Color(0xff006aff),
textTheme: ButtonTextTheme.accent,
colorScheme: theme.colorScheme.copyWith(
secondary: isDark ? Color(0xff005eff) : Color(0xffffffff),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(26),
),
),
primaryIconTheme: IconThemeData(color: colorTheme.black.withOpacity(.5)),
defaultChannelImage: (context, channel) => SizedBox(),
defaultUserImage: (context, user) => Center(
child: CachedNetworkImage(
filterQuality: FilterQuality.high,
imageUrl: getRandomPicUrl(user),
fit: BoxFit.cover,
),
),
channelPreviewTheme: ChannelPreviewTheme(
unreadCounterColor: colorTheme.accentRed,
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor(
height: 40,
width: 40,
),
),
title: textTheme.bodyBold,
subtitle: textTheme.footnote.copyWith(
color: Color(0xff7A7A7A),
),
lastMessageAt: textTheme.footnote.copyWith(
color: colorTheme.black.withOpacity(.5),
),
indicatorIconSize: 16.0),
channelTheme: ChannelTheme(
messageInputButtonIconTheme: theme.iconTheme.copyWith(
color: accentColor,
),
channelHeaderTheme: ChannelHeaderTheme(
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor(
height: 40,
width: 40,
),
),
color: colorTheme.white,
title: TextStyle(
fontSize: 14,
color: colorTheme.black,
),
lastMessageAt: TextStyle(
fontSize: 11,
color: colorTheme.black.withOpacity(.5),
),
),
inputBackground: colorTheme.white.withAlpha(12),
),
ownMessageTheme: MessageTheme(
messageText: TextStyle(
fontSize: 14.5,
color: colorTheme.black,
),
createdAt: TextStyle(
color: colorTheme.black.withOpacity(.5),
fontSize: 12,
),
replies: TextStyle(
color: accentColor,
fontWeight: FontWeight.w600,
fontSize: 12,
),
messageBackgroundColor: colorTheme.greyGainsboro,
reactionsBackgroundColor: colorTheme.white,
reactionsBorderColor: colorTheme.greyWhisper,
messageBorderColor: colorTheme.greyGainsboro,
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor(
height: 32,
width: 32,
),
),
messageLinks: TextStyle(
color: accentColor,
),
),
otherMessageTheme: MessageTheme(
reactionsBackgroundColor: colorTheme.greyGainsboro,
reactionsBorderColor: colorTheme.white,
messageText: TextStyle(
fontSize: 14.5,
color: colorTheme.black,
),
createdAt: TextStyle(
color: colorTheme.black.withOpacity(.5),
fontSize: 12,
),
replies: TextStyle(
color: accentColor,
fontWeight: FontWeight.w600,
fontSize: 12,
),
messageLinks: TextStyle(
color: accentColor,
),
messageBackgroundColor: colorTheme.white,
messageBorderColor: colorTheme.greyWhisper,
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor(
height: 32,
width: 32,
),
),
),
reactionIcons: [
ReactionIcon(
type: 'love',
assetName: 'Icon_love_reaction.svg',
),
ReactionIcon(
type: 'like',
assetName: 'Icon_thumbs_up_reaction.svg',
),
ReactionIcon(
type: 'sad',
assetName: 'Icon_thumbs_down_reaction.svg',
),
ReactionIcon(
type: 'haha',
assetName: 'Icon_LOL_reaction.svg',
),
ReactionIcon(
type: 'wow',
assetName: 'Icon_wut_reaction.svg',
),
],
);
}
}
enum TextThemeType {
light,
dark,
}
class TextTheme {
final TextStyle title;
final TextStyle headlineBold;
final TextStyle headline;
final TextStyle bodyBold;
final TextStyle body;
final TextStyle footnoteBold;
final TextStyle footnote;
final TextStyle captionBold;
TextTheme.light({
this.title = const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.black,
),
this.headlineBold = const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black,
),
this.headline = const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.black,
),
this.bodyBold = const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.black,
),
this.body = const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.black,
),
this.footnoteBold = const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.black,
),
this.footnote = const TextStyle(
fontSize: 12,
color: Colors.black,
),
this.captionBold = const TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.black,
),
});
TextTheme.dark({
this.title = const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.white,
),
this.headlineBold = const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
this.headline = const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.white,
),
this.bodyBold = const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.white,
),
this.body = const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white,
),
this.footnoteBold = const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.white,
),
this.footnote = const TextStyle(
fontSize: 12,
color: Colors.white,
),
this.captionBold = const TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.white,
),
});
TextTheme copyWith({
TextThemeType type = TextThemeType.light,
TextStyle body,
TextStyle title,
TextStyle headlineBold,
TextStyle headline,
TextStyle bodyBold,
TextStyle footnoteBold,
TextStyle footnote,
TextStyle captionBold,
}) {
return type == TextThemeType.light
? TextTheme.light(
body: body ?? this.body,
title: title ?? this.title,
headlineBold: headlineBold ?? this.headlineBold,
headline: headline ?? this.headline,
bodyBold: bodyBold ?? this.bodyBold,
footnoteBold: footnoteBold ?? this.footnoteBold,
footnote: footnote ?? this.footnote,
captionBold: captionBold ?? this.captionBold,
)
: TextTheme.dark(
body: body ?? this.body,
title: title ?? this.title,
headlineBold: headlineBold ?? this.headlineBold,
headline: headline ?? this.headline,
bodyBold: bodyBold ?? this.bodyBold,
footnoteBold: footnoteBold ?? this.footnoteBold,
footnote: footnote ?? this.footnote,
captionBold: captionBold ?? this.captionBold,
);
}
TextTheme merge(TextTheme other) {
if (other == null) return this;
return copyWith(
body: body?.merge(other.body) ?? other.body,
title: title?.merge(other.title) ?? other.title,
headlineBold:
headlineBold?.merge(other.headlineBold) ?? other.headlineBold,
headline: headline?.merge(other.headline) ?? other.headline,
bodyBold: bodyBold?.merge(other.bodyBold) ?? other.bodyBold,
footnoteBold:
footnoteBold?.merge(other.footnoteBold) ?? other.footnoteBold,
footnote: footnote?.merge(other.footnote) ?? other.footnote,
captionBold: captionBold?.merge(other.captionBold) ?? other.captionBold,
);
}
}
enum ColorThemeType {
light,
dark,
}
class ColorTheme {
final Color black;
final Color grey;
final Color greyGainsboro;
final Color greyWhisper;
final Color whiteSmoke;
final Color whiteSnow;
final Color white;
final Color blueAlice;
final Color accentBlue;
final Color accentRed;
final Color accentGreen;
final Effect borderTop;
final Effect borderBottom;
final Effect shadowIconButton;
final Effect modalShadow;
final Color highlight;
final Color overlay;
final Color overlayDark;
final Gradient bgGradient;
ColorTheme.light({
this.black = const Color(0xff000000),
this.grey = const Color(0xff7a7a7a),
this.greyGainsboro = const Color(0xffdbdbdb),
this.greyWhisper = const Color(0xffecebeb),
this.whiteSmoke = const Color(0xfff2f2f2),
this.whiteSnow = const Color(0xfffcfcfc),
this.white = const Color(0xffffffff),
this.blueAlice = const Color(0xffe9f2ff),
this.accentBlue = const Color(0xff005FFF),
this.accentRed = const Color(0xffFF3842),
this.accentGreen = const Color(0xff20E070),
this.highlight = const Color(0xfffbf4dd),
this.overlay = const Color.fromRGBO(0, 0, 0, 0.2),
this.overlayDark = const Color.fromRGBO(0, 0, 0, 0.6),
this.bgGradient = const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [const Color(0xfff7f7f7), const Color(0xfffcfcfc)],
stops: [0, 1],
),
this.borderTop = const Effect(
sigmaX: 0, sigmaY: -1, color: Color(0xff141924), blur: 0.0),
this.borderBottom =
const Effect(sigmaX: 0, sigmaY: 1, color: Color(0xff141924), blur: 0.0),
this.shadowIconButton = const Effect(
sigmaX: 0, sigmaY: 2, color: Color(0xff000000), alpha: 0.5, blur: 4.0),
this.modalShadow = const Effect(
sigmaX: 0, sigmaY: 0, color: Color(0xff000000), alpha: 1, blur: 8.0),
});
ColorTheme.dark({
this.black = const Color(0xffffffff),
this.grey = const Color(0xff7a7a7a),
this.greyGainsboro = const Color(0xff2d2f2f),
this.greyWhisper = const Color(0xff1c1e22),
this.whiteSmoke = const Color(0xff13151b),
this.whiteSnow = const Color(0xff070A0D),
this.white = const Color(0xff101418),
this.blueAlice = const Color(0xff00193D),
this.accentBlue = const Color(0xff005FFF),
this.accentRed = const Color(0xffFF3742),
this.accentGreen = const Color(0xff20E070),
this.borderTop = const Effect(
sigmaX: 0, sigmaY: -1, color: Color(0xff141924), blur: 0.0),
this.borderBottom =
const Effect(sigmaX: 0, sigmaY: 1, color: Color(0xff141924), blur: 0.0),
this.shadowIconButton = const Effect(
sigmaX: 0, sigmaY: 2, color: Color(0xff000000), alpha: 0.5, blur: 4.0),
this.modalShadow = const Effect(
sigmaX: 0, sigmaY: 0, color: Color(0xff000000), alpha: 1, blur: 8.0),
this.highlight = const Color(0xff302d22),
this.overlay = const Color.fromRGBO(0, 0, 0, 0.4),
this.overlayDark = const Color.fromRGBO(255, 255, 255, 0.6),
this.bgGradient = const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [const Color(0xff101214), const Color(0xff070a0d)],
stops: [0, 1],
),
});
ColorTheme copyWith({
ColorThemeType type = ColorThemeType.light,
Color black,
Color grey,
Color greyGainsboro,
Color greyWhisper,
Color whiteSmoke,
Color whiteSnow,
Color white,
Color blueAlice,
Color accentBlue,
Color accentRed,
Color accentGreen,
Effect borderTop,
Effect borderBottom,
Effect shadowIconButton,
Effect modalShadow,
Color highlight,
Color overlay,
Color overlayDark,
Gradient bgGradient,
}) {
return type == ColorThemeType.light
? ColorTheme.light(
black: black ?? this.black,
grey: grey ?? this.grey,
greyGainsboro: greyGainsboro ?? this.greyGainsboro,
greyWhisper: greyWhisper ?? this.greyWhisper,
whiteSmoke: whiteSmoke ?? this.whiteSmoke,
whiteSnow: whiteSnow ?? this.whiteSnow,
white: white ?? this.white,
blueAlice: blueAlice ?? this.blueAlice,
accentBlue: accentBlue ?? this.accentBlue,
accentRed: accentRed ?? this.accentRed,
accentGreen: accentGreen ?? this.accentGreen,
borderTop: borderTop ?? this.borderTop,
borderBottom: borderBottom ?? this.borderBottom,
shadowIconButton: shadowIconButton ?? this.shadowIconButton,
modalShadow: modalShadow ?? this.modalShadow,
highlight: highlight ?? this.highlight,
overlay: overlay ?? this.overlay,
overlayDark: overlayDark ?? this.overlayDark,
bgGradient: bgGradient ?? this.bgGradient,
)
: ColorTheme.dark(
black: black ?? this.black,
grey: grey ?? this.grey,
greyGainsboro: greyGainsboro ?? this.greyGainsboro,
greyWhisper: greyWhisper ?? this.greyWhisper,
whiteSmoke: whiteSmoke ?? this.whiteSmoke,
whiteSnow: whiteSnow ?? this.whiteSnow,
white: white ?? this.white,
blueAlice: blueAlice ?? this.blueAlice,
accentBlue: accentBlue ?? this.accentBlue,
accentRed: accentRed ?? this.accentRed,
accentGreen: accentGreen ?? this.accentGreen,
borderTop: borderTop ?? this.borderTop,
borderBottom: borderBottom ?? this.borderBottom,
shadowIconButton: shadowIconButton ?? this.shadowIconButton,
modalShadow: modalShadow ?? this.modalShadow,
highlight: highlight ?? this.highlight,
overlay: overlay ?? this.overlay,
overlayDark: overlayDark ?? this.overlayDark,
bgGradient: bgGradient ?? this.bgGradient,
);
}
ColorTheme merge(ColorTheme other) {
if (other == null) return this;
return copyWith(
black: other.black,
grey: other.grey,
greyGainsboro: other.greyGainsboro,
greyWhisper: other.greyWhisper,
whiteSmoke: other.whiteSmoke,
whiteSnow: other.whiteSnow,
white: other.white,
blueAlice: other.blueAlice,
accentBlue: other.accentBlue,
accentRed: other.accentRed,
accentGreen: other.accentGreen,
highlight: other.highlight,
overlay: other.overlay,
overlayDark: other.overlayDark,
bgGradient: other.bgGradient,
borderTop: other.borderTop,
borderBottom: other.borderBottom,
shadowIconButton: other.shadowIconButton,
modalShadow: other.modalShadow,
);
}
}
/// Channel theme data
class ChannelTheme {
/// Theme of the [ChannelHeader] widget
final ChannelHeaderTheme channelHeaderTheme;
/// IconTheme of the send button in [MessageInput]
final IconThemeData messageInputButtonIconTheme;
/// Theme of the send button in [MessageInput]
final ButtonThemeData messageInputButtonTheme;
/// Background color of [MessageInput]
final Color inputBackground;
ChannelTheme({
this.channelHeaderTheme,
this.messageInputButtonIconTheme,
this.messageInputButtonTheme,
this.inputBackground,
});
/// Creates a copy of [ChannelTheme] with specified attributes overridden.
ChannelTheme copyWith({
ChannelHeaderTheme channelHeaderTheme,
IconThemeData messageInputButtonIconTheme,
ButtonThemeData messageInputButtonTheme,
Color inputBackground,
}) =>
ChannelTheme(
channelHeaderTheme: channelHeaderTheme ?? this.channelHeaderTheme,
messageInputButtonIconTheme:
messageInputButtonIconTheme ?? this.messageInputButtonIconTheme,
messageInputButtonTheme:
messageInputButtonTheme ?? this.messageInputButtonTheme,
inputBackground: inputBackground ?? this.inputBackground,
);
ChannelTheme merge(ChannelTheme other) {
if (other == null) return this;
return copyWith(
channelHeaderTheme: channelHeaderTheme?.merge(other.channelHeaderTheme) ??
other.channelHeaderTheme,
messageInputButtonIconTheme: messageInputButtonIconTheme
?.merge(other.messageInputButtonIconTheme) ??
other.messageInputButtonIconTheme,
messageInputButtonTheme: other.messageInputButtonTheme,
inputBackground: other.inputBackground,
);
}
}
class AvatarTheme {
final BoxConstraints constraints;
final BorderRadius borderRadius;
AvatarTheme({
this.constraints,
this.borderRadius,
});
AvatarTheme copyWith({
BoxConstraints constraints,
BorderRadius borderRadius,
}) =>
AvatarTheme(
constraints: constraints ?? this.constraints,
borderRadius: borderRadius ?? this.borderRadius,
);
AvatarTheme merge(AvatarTheme other) {
if (other == null) return this;
return copyWith(
constraints: other.constraints,
borderRadius: other.borderRadius,
);
}
}
class MessageTheme {
final TextStyle messageText;
final TextStyle messageAuthor;
final TextStyle messageLinks;
final TextStyle createdAt;
final TextStyle replies;
final Color messageBackgroundColor;
final Color messageBorderColor;
final Color reactionsBackgroundColor;
final Color reactionsBorderColor;
final AvatarTheme avatarTheme;
const MessageTheme({
this.replies,
this.messageText,
this.messageAuthor,
this.messageLinks,
this.messageBackgroundColor,
this.messageBorderColor,
this.reactionsBackgroundColor,
this.reactionsBorderColor,
this.avatarTheme,
this.createdAt,
});
MessageTheme copyWith({
TextStyle messageText,
TextStyle messageAuthor,
TextStyle messageLinks,
TextStyle createdAt,
TextStyle replies,
Color messageBackgroundColor,
Color messageBorderColor,
AvatarTheme avatarTheme,
Color reactionsBackgroundColor,
Color reactionsBorderColor,
}) =>
MessageTheme(
messageText: messageText ?? this.messageText,
messageAuthor: messageAuthor ?? this.messageAuthor,
messageLinks: messageLinks ?? this.messageLinks,
createdAt: createdAt ?? this.createdAt,
messageBackgroundColor:
messageBackgroundColor ?? this.messageBackgroundColor,
messageBorderColor: messageBorderColor ?? this.messageBorderColor,
avatarTheme: avatarTheme ?? this.avatarTheme,
replies: replies ?? this.replies,
reactionsBackgroundColor:
reactionsBackgroundColor ?? this.reactionsBackgroundColor,
reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor,
);
MessageTheme merge(MessageTheme other) {
if (other == null) return this;
return copyWith(
messageText: messageText?.merge(other.messageText) ?? other.messageText,
messageAuthor:
messageAuthor?.merge(other.messageAuthor) ?? other.messageAuthor,
messageLinks:
messageLinks?.merge(other.messageLinks) ?? other.messageLinks,
createdAt: createdAt?.merge(other.createdAt) ?? other.createdAt,
replies: replies?.merge(other.replies) ?? other.replies,
messageBackgroundColor: other.messageBackgroundColor,
messageBorderColor: other.messageBorderColor,
avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme,
reactionsBackgroundColor: other.reactionsBackgroundColor,
reactionsBorderColor: other.reactionsBorderColor,
);
}
}
class ChannelPreviewTheme {
final TextStyle title;
final TextStyle subtitle;
final TextStyle lastMessageAt;
final AvatarTheme avatarTheme;
final Color unreadCounterColor;
final double indicatorIconSize;
const ChannelPreviewTheme({
this.title,
this.subtitle,
this.lastMessageAt,
this.avatarTheme,
this.unreadCounterColor,
this.indicatorIconSize,
});
ChannelPreviewTheme copyWith({
TextStyle title,
TextStyle subtitle,
TextStyle lastMessageAt,
AvatarTheme avatarTheme,
Color unreadCounterColor,
double indicatorIconSize,
}) =>
ChannelPreviewTheme(
title: title ?? this.title,
subtitle: subtitle ?? this.subtitle,
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
avatarTheme: avatarTheme ?? this.avatarTheme,
unreadCounterColor: unreadCounterColor ?? this.unreadCounterColor,
indicatorIconSize: indicatorIconSize ?? this.indicatorIconSize,
);
ChannelPreviewTheme merge(ChannelPreviewTheme other) {
if (other == null) return this;
return copyWith(
title: title?.merge(other.title) ?? other.title,
subtitle: subtitle?.merge(other.subtitle) ?? other.subtitle,
lastMessageAt:
lastMessageAt?.merge(other.lastMessageAt) ?? other.lastMessageAt,
avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme,
unreadCounterColor: other.unreadCounterColor,
);
}
}
class ChannelHeaderTheme {
final TextStyle title;
final TextStyle lastMessageAt;
final AvatarTheme avatarTheme;
final Color color;
const ChannelHeaderTheme({
this.title,
this.lastMessageAt,
this.avatarTheme,
this.color,
});
ChannelHeaderTheme copyWith({
TextStyle title,
TextStyle lastMessageAt,
AvatarTheme avatarTheme,
Color color,
}) =>
ChannelHeaderTheme(
title: title ?? this.title,
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
avatarTheme: avatarTheme ?? this.avatarTheme,
color: color ?? this.color,
);
ChannelHeaderTheme merge(ChannelHeaderTheme other) {
if (other == null) return this;
return copyWith(
title: title?.merge(other.title) ?? other.title,
lastMessageAt:
lastMessageAt?.merge(other.lastMessageAt) ?? other.lastMessageAt,
avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme,
color: other.color,
);
}
}
class Effect {
final double sigmaX;
final double sigmaY;
final Color color;
final double alpha;
final double blur;
const Effect({
this.sigmaX,
this.sigmaY,
this.color,
this.alpha,
this.blur,
});
Effect copyWith({
double sigmaX,
double sigmaY,
Color color,
double alpha,
double blur,
}) =>
Effect(
sigmaX: sigmaX ?? this.sigmaX,
sigmaY: sigmaY ?? this.sigmaY,
color: color ?? this.color,
alpha: color ?? this.alpha,
blur: blur ?? this.blur,
);
}
-40
View File
@@ -1,40 +0,0 @@
import 'package:flutter/material.dart';
class StreamNeumorphicButton extends StatelessWidget {
final Widget child;
final Color backgroundColor;
const StreamNeumorphicButton({
Key key,
@required this.child,
this.backgroundColor = Colors.white,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
child: child,
margin: EdgeInsets.all(8.0),
height: 40,
width: 40,
decoration: BoxDecoration(
color: backgroundColor,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.grey[700],
offset: Offset(0, 1.0),
blurRadius: 0.5,
spreadRadius: 0,
),
BoxShadow(
color: Colors.white,
offset: Offset.zero,
blurRadius: 0.5,
spreadRadius: 0,
),
],
),
);
}
}
-832
View File
@@ -1,832 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class StreamSvgIcon extends StatelessWidget {
final String assetName;
final double width;
final double height;
final Color color;
const StreamSvgIcon({
this.assetName,
this.color,
this.width = 24,
this.height = 24,
});
@override
Widget build(BuildContext context) {
final key = Key('StreamSvgIcon-$assetName');
return kIsWeb
? Image.network(
'packages/stream_chat_flutter/svgs/$assetName',
width: width,
height: height,
key: key,
color: color,
alignment: Alignment.center,
)
: SvgPicture.asset(
'lib/svgs/$assetName',
package: 'stream_chat_flutter',
key: key,
width: width,
height: height,
color: color,
alignment: Alignment.center,
);
}
factory StreamSvgIcon.settings({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'settings.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.down({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_down.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.attach({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_attach.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.smile({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_smile.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.mentions({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'mentions.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.record({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_record.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.camera({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_camera.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.files({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'files.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.pictures({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'pictures.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.left({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_left.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.user({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_user.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.userAdd({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_User_add.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.check({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_check.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.checkAll({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_check_all.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.checkSend({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_check_send.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.penWrite({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_pen-write.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.contacts({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_contacts.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.close({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_close.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.search({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_search.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.right({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_right.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.mute({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_mute.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.userRemove({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_User_deselect.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.lightning({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_lightning-command runner.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.emptyCircleLeft({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_empty_circle_left.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.message({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_message.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.thread({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_Thread_Reply.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.reply({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_curve_line_left_up_big.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.edit({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_edit.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.download({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_download.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.cloud_download({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_cloud_download.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.copy({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_copy.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.delete({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_delete.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.eye({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_eye-off.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.arrow_right({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_arrow_right.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.close_small({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_close_sml.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.Icon_curve_line_left_up({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_curve_line_left_up.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.icon_SHARE({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'icon_SHARE.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.Icon_grid({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_grid.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.Icon_send_message({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_send_message.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.Icon_menu_point_v({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_menu_point_v.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.Icon_save({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_save.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.share_arrow({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'share_arrow.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_7z({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_7z.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_csv({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_CSV.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_doc({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_DOC.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_docx({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_DOCX.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_Generic({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_Generic.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_html({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_html.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_md({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_MD.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_odt({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_ODT.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_pdf({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_PDF.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_ppt({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_PPT.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_pptx({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_PPTX.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_rar({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_RAR.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_rtf({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_RTF.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_tar({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_TAR.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_txt({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_TXT.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_xls({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_XLS.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_xlsx({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_XLSX.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.filetype_zip({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'filetype_ZIP.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.Icon_group({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_group.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.Icon_notification({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_notification.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.Icon_user_delete({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_user_delete.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.error({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_error.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.circle_up({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_circle_up.svg',
color: color,
width: size,
height: size,
);
}
factory StreamSvgIcon.Icon_user_settings({
double size,
Color color,
}) {
return StreamSvgIcon(
assetName: 'Icon_user_settings.svg',
color: color,
width: size,
height: size,
);
}
}
-163
View File
@@ -1,163 +0,0 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'stream_chat_theme.dart';
///
class Swipeable extends StatefulWidget {
final Widget child;
final Widget backgroundIcon;
final VoidCallback onSwipeStart;
final VoidCallback onSwipeCancel;
final VoidCallback onSwipeEnd;
final double threshold;
///
const Swipeable({
@required this.child,
@required this.backgroundIcon,
this.onSwipeStart,
this.onSwipeCancel,
this.onSwipeEnd,
this.threshold = 82.0,
});
@override
State<StatefulWidget> createState() => _SwipeableState();
}
class _SwipeableState extends State<Swipeable> with TickerProviderStateMixin {
double _dragExtent = 0.0;
AnimationController _moveController;
AnimationController _iconMoveController;
Animation<Offset> _moveAnimation;
Animation<Offset> _iconTransitionAnimation;
Animation<double> _iconFadeAnimation;
bool _pastThreshold = false;
final _animationDuration = const Duration(milliseconds: 200);
@override
void initState() {
super.initState();
_moveController =
AnimationController(duration: _animationDuration, vsync: this);
_iconMoveController =
AnimationController(duration: _animationDuration, vsync: this);
_moveAnimation = Tween<Offset>(begin: Offset.zero, end: Offset(1.0, 0.0))
.animate(_moveController);
_iconTransitionAnimation =
Tween<Offset>(begin: Offset(-0.1, 0.0), end: Offset(0.4, 0.0))
.animate(_moveController);
_iconFadeAnimation =
Tween<double>(begin: 0.7, end: 1.0).animate(_iconMoveController);
final controllerValue = 0.0;
_moveController.animateTo(controllerValue);
_iconMoveController.animateTo(controllerValue);
}
@override
void dispose() {
_moveController.dispose();
_iconMoveController.dispose();
super.dispose();
}
void _handleDragStart(DragStartDetails details) {
if (widget.onSwipeStart != null) {
widget.onSwipeStart();
}
}
void _handleDragUpdate(DragUpdateDetails details) {
final delta = details.primaryDelta;
_dragExtent += delta;
if (_dragExtent.isNegative) return;
var movePastThresholdPixels = widget.threshold;
var newPos = _dragExtent.abs() / context.size.width;
if (_dragExtent.abs() > movePastThresholdPixels) {
// how many "thresholds" past the threshold we are. 1 = the threshold 2
// = two thresholds.
var n = _dragExtent.abs() / movePastThresholdPixels;
// Take the number of thresholds past the threshold, and reduce this
// number
var reducedThreshold = math.pow(n, 0.3);
var adjustedPixelPos = movePastThresholdPixels * reducedThreshold;
newPos = adjustedPixelPos / context.size.width;
if (_dragExtent > 0 && !_pastThreshold) {
_iconMoveController.value = 1;
_pastThreshold = true;
}
} else {
// Send a cancel event if the user has swiped back underneath the
// threshold
if (_pastThreshold && widget.onSwipeCancel != null) {
widget.onSwipeCancel();
}
_pastThreshold = false;
}
if (!_pastThreshold || newPos < _moveController.value) {
_iconMoveController.value = newPos;
}
_moveController.value = newPos;
}
void _handleDragEnd(DragEndDetails details) {
_moveController.animateTo(0.0, duration: _animationDuration);
_iconMoveController.animateTo(0.0, duration: _animationDuration);
_dragExtent = 0.0;
if (_pastThreshold && widget.onSwipeEnd != null) {
widget.onSwipeEnd();
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onHorizontalDragStart: _handleDragStart,
onHorizontalDragUpdate: _handleDragUpdate,
onHorizontalDragEnd: _handleDragEnd,
behavior: HitTestBehavior.opaque,
child: Stack(
alignment: Alignment.center,
fit: StackFit.passthrough,
children: [
SlideTransition(
position: _iconTransitionAnimation,
child: Row(
children: [
FadeTransition(
opacity: _iconFadeAnimation,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: StreamChatTheme.of(context)
.colorTheme
.greyGainsboro,
),
),
child: widget.backgroundIcon,
),
),
],
),
),
SlideTransition(
position: _moveAnimation,
child: widget.child,
),
],
),
);
}
}
-116
View File
@@ -1,116 +0,0 @@
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// It shows a date divider depending on the date difference
class SystemMessage extends StatelessWidget {
/// This message
final Message message;
/// The function called when tapping on the message when the message is not failed
final void Function(Message) onMessageTap;
const SystemMessage({
Key key,
@required this.message,
this.onMessageTap,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final divider = Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Divider(),
),
);
final createdAt = Jiffy(message.createdAt.toLocal());
final now = DateTime.now();
final hourInfo = createdAt.format('h:mm a');
String dayInfo;
if (Jiffy(createdAt).isSame(now, Units.DAY)) {
dayInfo = 'TODAY';
} else if (Jiffy(createdAt)
.isSame(now.subtract(Duration(days: 1)), Units.DAY)) {
dayInfo = 'YESTERDAY';
} else if (Jiffy(createdAt).isAfter(
now.subtract(Duration(days: 7)),
Units.DAY,
)) {
dayInfo = createdAt.format('EEEE').toUpperCase();
} else if (Jiffy(createdAt).isAfter(
Jiffy(now).subtract(years: 1),
Units.DAY,
)) {
dayInfo = createdAt.format('dd/MM').toUpperCase();
} else {
dayInfo = createdAt.format('dd/MM/yyyy').toUpperCase();
}
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (onMessageTap != null) {
onMessageTap(message);
}
},
child: Container(
width: double.infinity,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
divider,
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(
message.text,
style: TextStyle(
fontSize: 10,
color: Theme.of(context)
.textTheme
.headline6
.color
.withOpacity(.5),
fontWeight: FontWeight.bold,
),
),
Text.rich(
TextSpan(
children: [
TextSpan(
text: dayInfo,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
TextSpan(text: ' AT'),
TextSpan(text: ' $hourInfo'),
],
style: TextStyle(
fontWeight: FontWeight.normal,
),
),
style: TextStyle(
fontSize: 10,
color: Theme.of(context)
.textTheme
.headline6
.color
.withOpacity(.5),
),
),
],
),
),
divider,
],
),
),
);
}
}
-132
View File
@@ -1,132 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'back_button.dart';
import 'channel_name.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/thread_header.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/thread_header_paint.png)
///
/// It shows the current thread information.
///
/// ```dart
/// class ThreadPage extends StatelessWidget {
/// final Message parent;
///
/// ThreadPage({
/// Key key,
/// this.parent,
/// }) : super(key: key);
///
/// @override
/// Widget build(BuildContext context) {
/// return Scaffold(
/// appBar: ThreadHeader(
/// parent: parent,
/// ),
/// body: Column(
/// children: <Widget>[
/// Expanded(
/// child: MessageListView(
/// parentMessage: parent,
/// ),
/// ),
/// MessageInput(
/// parentMessage: parent,
/// ),
/// ],
/// ),
/// );
/// }
/// }
/// ```
///
/// Usually you would use this widget as an [AppBar] inside a [Scaffold].
/// However you can also use it as a normal widget.
///
/// Make sure to have a [StreamChannel] ancestor in order to provide the information about the channel.
/// Every part of the widget uses a [StreamBuilder] to render the channel information as soon as it updates.
///
/// By default the widget shows a backButton that calls [Navigator.pop].
/// You can disable this button using the [showBackButton] property of just override the behaviour
/// with [onBackPressed].
///
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property.
/// Modify it to change the widget appearance.
class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
/// True if this header shows the leading back button
final bool showBackButton;
/// Callback to call when pressing the back button.
/// By default it calls [Navigator.pop]
final VoidCallback onBackPressed;
/// The message parent of this thread
final Message parent;
/// Instantiate a new ThreadHeader
ThreadHeader({
Key key,
@required this.parent,
this.showBackButton = true,
this.onBackPressed,
}) : preferredSize = Size.fromHeight(kToolbarHeight),
super(key: key);
@override
Widget build(BuildContext context) {
return AppBar(
automaticallyImplyLeading: false,
elevation: 1,
leading: showBackButton
? StreamBackButton(
onPressed: onBackPressed,
showUnreads: true,
)
: SizedBox(),
backgroundColor:
StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color,
centerTitle: true,
title: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Thread Reply',
style: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.title,
),
SizedBox(height: 2),
Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'with ',
style: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.lastMessageAt,
),
Flexible(
child: ChannelName(
textStyle: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.lastMessageAt,
),
),
],
),
],
),
);
}
@override
final Size preferredSize;
}
-76
View File
@@ -1,76 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lottie/lottie.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_channel.dart';
/// Widget to show the current list of typing users
class TypingIndicator extends StatelessWidget {
/// Instantiate a new TypingIndicator
const TypingIndicator({
Key key,
this.channel,
this.alternativeWidget,
this.style,
this.alignment = Alignment.centerLeft,
this.padding = const EdgeInsets.all(0),
}) : super(key: key);
/// Style of the text widget
final TextStyle style;
/// List of typing users
final Channel channel;
/// Widget built when no typings is happening
final Widget alternativeWidget;
/// The padding of this widget
final EdgeInsets padding;
final Alignment alignment;
@override
Widget build(BuildContext context) {
final channelState =
channel?.state ?? StreamChannel.of(context).channel.state;
return StreamBuilder<List<User>>(
initialData: channelState.typingEvents,
stream: channelState.typingEventsStream,
builder: (context, snapshot) {
return AnimatedSwitcher(
duration: Duration(milliseconds: 300),
child: snapshot.data?.isNotEmpty == true
? Padding(
padding: padding,
child: Align(
key: Key('typings'),
alignment: alignment,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Lottie.asset(
'animations/typing_dots.json',
package: 'stream_chat_flutter',
height: 4,
),
Text(
' ${snapshot.data.map((u) => u.name).join(',')} ${snapshot.data.length == 1 ? 'is' : 'are'} typing',
maxLines: 1,
style: style,
),
],
),
),
)
: Align(
key: Key('alternative'),
alignment: alignment,
child: Container(
child: alternativeWidget ?? Offstage(),
),
),
);
},
);
}
}
-46
View File
@@ -1,46 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class UnreadIndicator extends StatelessWidget {
const UnreadIndicator({
Key key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final client = StreamChat.of(context).client;
return StreamBuilder<int>(
stream: client.state.totalUnreadCountStream,
initialData: client.state.totalUnreadCount,
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data == 0) {
return SizedBox();
}
return Material(
borderRadius: BorderRadius.circular(8),
color: StreamChatTheme.of(context)
.channelPreviewTheme
.unreadCounterColor,
child: Padding(
padding: const EdgeInsets.only(
left: 5.0,
right: 5.0,
top: 2,
bottom: 1,
),
child: Center(
child: Text(
'${snapshot.data}',
style: TextStyle(
fontSize: 11,
color: Colors.white,
),
),
),
),
);
},
);
}
}
-108
View File
@@ -1,108 +0,0 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/utils.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class UrlAttachment extends StatelessWidget {
final Attachment urlAttachment;
final String hostDisplayName;
final EdgeInsets textPadding;
UrlAttachment({
@required this.urlAttachment,
@required this.hostDisplayName,
@required this.textPadding,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => launchURL(
context,
urlAttachment.ogScrapeUrl,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (urlAttachment.imageUrl != null)
SizedBox(
height: 16.0,
),
if (urlAttachment.imageUrl != null)
Container(
clipBehavior: Clip.antiAliasWithSaveLayer,
margin: EdgeInsets.symmetric(horizontal: 8.0),
child: Stack(
children: [
Center(
child: CachedNetworkImage(
imageUrl: urlAttachment.imageUrl,
),
),
Positioned(
left: 0.0,
bottom: -1,
child: Container(
child: Padding(
padding: const EdgeInsets.only(
top: 8.0,
left: 8.0,
right: 8.0,
),
child: Text(
hostDisplayName,
style: StreamChatTheme.of(context)
.textTheme
.bodyBold
.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.accentBlue,
),
),
),
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topRight: Radius.circular(16.0),
),
color: StreamChatTheme.of(context).colorTheme.blueAlice,
),
),
),
],
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.0),
),
),
Padding(
padding: textPadding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (urlAttachment.title != null)
Text(
urlAttachment.title.trim(),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: StreamChatTheme.of(context)
.textTheme
.body
.copyWith(fontWeight: FontWeight.w700),
),
if (urlAttachment.text != null)
Text(
urlAttachment.text,
style: StreamChatTheme.of(context)
.textTheme
.body
.copyWith(fontWeight: FontWeight.w400),
),
],
),
),
],
),
);
}
}
-114
View File
@@ -1,114 +0,0 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import '../stream_chat_flutter.dart';
class UserAvatar extends StatelessWidget {
const UserAvatar({
Key key,
@required this.user,
this.constraints,
this.onlineIndicatorConstraints,
this.onTap,
this.onLongPress,
this.showOnlineStatus = true,
this.borderRadius,
this.onlineIndicatorAlignment = Alignment.topRight,
this.selected = false,
this.selectionColor,
this.selectionThickness = 4,
}) : super(key: key);
final User user;
final Alignment onlineIndicatorAlignment;
final BoxConstraints constraints;
final BorderRadius borderRadius;
final BoxConstraints onlineIndicatorConstraints;
final void Function(User) onTap;
final void Function(User) onLongPress;
final bool showOnlineStatus;
final bool selected;
final Color selectionColor;
final double selectionThickness;
@override
Widget build(BuildContext context) {
final hasImage = user.extraData?.containsKey('image') == true &&
user.extraData['image'] != null &&
user.extraData['image'] != '';
final streamChatTheme = StreamChatTheme.of(context);
Widget avatar = ClipRRect(
clipBehavior: Clip.antiAlias,
borderRadius: borderRadius ??
streamChatTheme.ownMessageTheme.avatarTheme.borderRadius,
child: Container(
constraints: constraints ??
streamChatTheme.ownMessageTheme.avatarTheme.constraints,
decoration: BoxDecoration(
color: streamChatTheme.colorTheme.accentBlue,
),
child: hasImage
? CachedNetworkImage(
filterQuality: FilterQuality.high,
imageUrl: user.extraData['image'],
errorWidget: (_, __, ___) {
return streamChatTheme.defaultUserImage(context, user);
},
fit: BoxFit.cover,
)
: streamChatTheme.defaultUserImage(context, user),
),
);
if (selected) {
avatar = ClipRRect(
borderRadius: (borderRadius ??
streamChatTheme.ownMessageTheme.avatarTheme.borderRadius) +
BorderRadius.circular(selectionThickness),
child: Container(
constraints: constraints ??
streamChatTheme.ownMessageTheme.avatarTheme.constraints,
color: selectionColor ??
StreamChatTheme.of(context).colorTheme.accentBlue,
child: Padding(
padding: EdgeInsets.all(selectionThickness),
child: avatar,
),
),
);
}
return GestureDetector(
onTap: onTap != null ? () => onTap(user) : null,
onLongPress: onLongPress != null ? () => onLongPress(user) : null,
child: Stack(
children: <Widget>[
avatar,
if (showOnlineStatus && user.online == true)
Positioned.fill(
child: Align(
alignment: onlineIndicatorAlignment,
child: Material(
type: MaterialType.circle,
child: Container(
padding: const EdgeInsets.all(2.0),
constraints: onlineIndicatorConstraints ??
BoxConstraints.tightFor(
width: 12,
height: 12,
),
child: Material(
shape: CircleBorder(),
color: streamChatTheme.colorTheme.accentGreen,
),
),
color: StreamChatTheme.of(context).colorTheme.white,
),
),
),
],
),
);
}
}
-97
View File
@@ -1,97 +0,0 @@
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/user_list_view.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'stream_chat_theme.dart';
///
/// It shows the current [User] preview.
///
/// The widget uses a [StreamBuilder] to render the user information image as soon as it updates.
///
/// Usually you don't use this widget as it's the default user preview used by [UserListView].
///
/// The widget renders the ui based on the first ancestor of type [StreamChatTheme].
/// Modify it to change the widget appearance.
class UserItem extends StatelessWidget {
/// Instantiate a new UserItem
const UserItem({
Key key,
@required this.user,
this.onTap,
this.onLongPress,
this.onImageTap,
this.selected = false,
this.showLastOnline = true,
}) : super(key: key);
/// Function called when tapping this widget
final void Function(User) onTap;
/// Function called when long pressing this widget
final void Function(User) onLongPress;
/// User displayed
final User user;
/// The function called when the image is tapped
final void Function(User) onImageTap;
/// If true the [UserItem] will show a trailing checkmark
final bool selected;
/// If true the [UserItem] will show the last seen
final bool showLastOnline;
@override
Widget build(BuildContext context) {
return ListTile(
onTap: () {
if (onTap != null) {
onTap(user);
}
},
onLongPress: () {
if (onLongPress != null) {
onLongPress(user);
}
},
leading: UserAvatar(
user: user,
showOnlineStatus: true,
onTap: (user) {
if (onImageTap != null) {
onImageTap(user);
}
},
constraints: BoxConstraints.tightFor(
height: 40,
width: 40,
),
),
trailing: selected
? StreamSvgIcon.checkSend(
color: StreamChatTheme.of(context).colorTheme.accentBlue,
)
: null,
title: Text(
user.name,
style: StreamChatTheme.of(context).textTheme.bodyBold,
),
subtitle: showLastOnline ? _buildLastActive(context) : null,
);
}
Widget _buildLastActive(context) {
return Text(
user.online == true
? 'Online'
: 'Last online ${Jiffy(user.lastActive).fromNow()}',
style: StreamChatTheme.of(context).textTheme.footnote.copyWith(
color: StreamChatTheme.of(context).colorTheme.black.withOpacity(.5)),
);
}
}
-548
View File
@@ -1,548 +0,0 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart';
import 'package:stream_chat_flutter/src/users_bloc.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'user_item.dart';
/// Callback called when tapping on a user
typedef UserTapCallback = void Function(User, Widget);
/// Builder used to create a custom [ListUserItem] from a [User]
typedef UserItemBuilder = Widget Function(BuildContext, User, bool);
///
/// It shows the list of current users.
///
/// ```dart
/// class UsersListPage extends StatelessWidget {
/// @override
/// Widget build(BuildContext context) {
/// return Scaffold(
/// body: UsersListView(
/// filter: {
/// 'members': {
/// '\$in': [StreamChat.of(context).user.id],
/// }
/// },
/// sort: [SortOption('last_message_at')],
/// pagination: PaginationParams(
/// limit: 20,
/// ),
/// channelWidget: ChannelPage(),
/// ),
/// );
/// }
/// }
/// ```
///
///
/// Make sure to have a [UsersBloc] ancestor in order to provide the information about the users.
/// The widget uses a [ListView.separated], [GridView.builder] to render the list, grid of channels.
///
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme].
/// Modify it to change the widget appearance.
class UserListView extends StatefulWidget {
/// Instantiate a new UserListView
const UserListView({
Key key,
this.errorBuilder,
this.emptyBuilder,
this.filter,
this.options,
this.sort,
this.pagination,
this.onUserTap,
this.onUserLongPress,
this.userWidget,
this.userItemBuilder,
this.separatorBuilder,
this.onImageTap,
this.selectedUsers,
this.pullToRefresh = true,
this.groupAlphabetically = false,
this.crossAxisCount = 1,
}) : assert(
crossAxisCount == 1 || groupAlphabetically == false,
'Cannot group alphabetically when crossAxisCount > 1',
),
super(key: key);
/// The builder that will be used in case of error
final Widget Function(Error error) errorBuilder;
/// The builder used when the channel list is empty.
final WidgetBuilder emptyBuilder;
/// The query filters to use.
/// You can query on any of the custom fields you've defined on the [Channel].
/// You can also filter other built-in channel fields.
final Map<String, dynamic> filter;
/// Query channels options.
///
/// state: if true returns the Channel state
/// watch: if true listen to changes to this Channel in real time.
final Map<String, dynamic> options;
/// The sorting used for the channels matching the filters.
/// Sorting is based on field and direction, multiple sorting options can be provided.
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
/// Direction can be ascending or descending.
final List<SortOption> sort;
/// Pagination parameters
/// limit: the number of users to return (max is 30)
/// offset: the offset (max is 1000)
/// message_limit: how many messages should be included to each channel
final PaginationParams pagination;
/// Function called when tapping on a channel
/// By default it calls [Navigator.push] building a [MaterialPageRoute]
/// with the widget [userWidget] as child.
final UserTapCallback onUserTap;
/// Function called when long pressing on a channel
final Function(User) onUserLongPress;
/// Widget used when opening a channel
final Widget userWidget;
/// Builder used to create a custom user preview
final UserItemBuilder userItemBuilder;
/// Builder used to create a custom item separator
final Function(BuildContext, int) separatorBuilder;
/// The function called when the image is tapped
final Function(User) onImageTap;
/// Set it to false to disable the pull-to-refresh widget
final bool pullToRefresh;
/// Sets a blue trailing checkMark in [ListUserItem] for all the [selectedUsers]
final Set<User> selectedUsers;
/// Set it to true to group users by their first character
///
/// defaults to false
final bool groupAlphabetically;
/// The number of children in the cross axis.
final int crossAxisCount;
@override
_UserListViewState createState() => _UserListViewState();
}
class _UserListViewState extends State<UserListView>
with WidgetsBindingObserver {
bool get _isListView => widget.crossAxisCount == 1;
@override
void initState() {
super.initState();
final usersBloc = UsersBloc.of(context);
usersBloc.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination,
options: widget.options,
);
}
@override
Widget build(BuildContext context) {
final usersBloc = UsersBloc.of(context);
if (!widget.pullToRefresh) {
return _buildListView(usersBloc);
}
return RefreshIndicator(
onRefresh: () async {
return usersBloc.queryUsers(
filter: widget.filter,
sort: widget.sort,
options: widget.options,
pagination: widget.pagination,
);
},
child: _buildListView(usersBloc),
);
}
bool get isListAlreadySorted =>
widget.sort?.any((e) => e.field == 'name' && e.direction == 1) ?? false;
Stream<List<ListItem>> _buildUserStream(
UsersBlocState usersBlocState,
) {
return usersBlocState.usersStream.map(
(users) {
if (widget.groupAlphabetically) {
var temp = users;
if (!isListAlreadySorted) {
temp = users..sort((curr, next) => curr.name.compareTo(next.name));
}
final groupedUsers = <String, List<User>>{};
for (var e in temp) {
final alphabet = e.name[0]?.toUpperCase();
groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e];
}
final items = <ListItem>[];
for (var key in groupedUsers.keys) {
items.add(ListHeaderItem(key));
items.addAll(groupedUsers[key].map((e) => ListUserItem(e)));
}
return items;
}
return users.map((e) => ListUserItem(e)).toList();
},
);
}
StreamBuilder<List<ListItem>> _buildListView(
UsersBlocState usersBlocState,
) {
return StreamBuilder(
stream: _buildUserStream(usersBlocState),
builder: (context, snapshot) {
if (snapshot.hasError) {
if (snapshot.error is Error) {
print((snapshot.error as Error).stackTrace);
}
if (widget.errorBuilder != null) {
return widget.errorBuilder(snapshot.error);
}
var message = snapshot.error.toString();
if (snapshot.error is DioError) {
final dioError = snapshot.error as DioError;
if (dioError.type == DioErrorType.RESPONSE) {
message = dioError.message;
} else {
message = 'Check your connection and retry';
}
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text.rich(
TextSpan(
children: [
WidgetSpan(
child: Padding(
padding: const EdgeInsets.only(
right: 2.0,
),
child: Icon(Icons.error_outline),
),
),
TextSpan(text: 'Error loading channels'),
],
),
style: Theme.of(context).textTheme.headline6,
),
Padding(
padding: const EdgeInsets.only(
top: 16.0,
),
child: Text(message),
),
FlatButton(
onPressed: () {
usersBlocState.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination,
options: widget.options,
);
},
child: Text('Retry'),
),
],
),
);
}
if (!snapshot.hasData) {
return LayoutBuilder(
builder: (context, viewportConstraints) {
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: Center(
child: CircularProgressIndicator(),
),
),
);
},
);
}
final items = snapshot.data;
if (items.isEmpty && widget.emptyBuilder != null) {
return widget.emptyBuilder(context);
}
if (items.isEmpty && widget.emptyBuilder == null) {
return LayoutBuilder(
builder: (context, viewportConstraints) {
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: Center(
child: Text('There are no users currently'),
),
),
);
},
);
}
final child = _isListView
? ListView.separated(
physics: AlwaysScrollableScrollPhysics(),
itemCount: items.isNotEmpty ? items.length + 1 : items.length,
separatorBuilder: (_, index) {
if (widget.separatorBuilder != null) {
return widget.separatorBuilder(context, index);
}
return _separatorBuilder(context, index);
},
itemBuilder: (context, index) {
return _listItemBuilder(context, index, items);
},
)
: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: widget.crossAxisCount,
),
itemCount: items.isNotEmpty ? items.length + 1 : items.length,
physics: AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index) {
return _gridItemBuilder(context, index, items);
},
);
return LazyLoadScrollView(
onEndOfPage: () async {
return _listenUserPagination(usersBlocState);
},
child: child,
);
},
);
}
Widget _listItemBuilder(BuildContext context, int i, List<ListItem> items) {
final usersProvider = UsersBloc.of(context);
if (i < items.length) {
final item = items[i];
return item.when(
headerItem: (header) {
return Container(
key: ValueKey<String>('HEADER-$header'),
color:
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.05),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6),
child: Text(
header,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14.5,
color: StreamChatTheme.of(context).colorTheme.grey,
),
),
),
);
},
userItem: (user) {
final selected = widget.selectedUsers?.contains(user) ?? false;
return Container(
key: ValueKey<String>('USER-${user.id}'),
child: widget.userItemBuilder != null
? widget.userItemBuilder(context, user, selected)
: UserItem(
user: user,
onTap: (user) => widget.onUserTap(user, widget.userWidget),
onLongPress: widget.onUserLongPress,
onImageTap: widget.onImageTap,
selected: selected,
),
);
},
);
} else {
return _buildQueryProgressIndicator(context, usersProvider);
}
}
Widget _gridItemBuilder(BuildContext context, int i, List<ListItem> items) {
final usersProvider = UsersBloc.of(context);
if (i < items.length) {
final item = items[i];
return item.when(
headerItem: (_) => Offstage(),
userItem: (user) {
final selected = widget.selectedUsers?.contains(user) ?? false;
return Container(
key: ValueKey<String>('USER-${user.id}'),
child: widget.userItemBuilder != null
? widget.userItemBuilder(context, user, selected)
: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
UserAvatar(
user: user,
borderRadius: BorderRadius.circular(32),
selected: selected,
constraints: BoxConstraints.tightFor(
height: 64,
width: 64,
),
onTap: (user) =>
widget.onUserTap(user, widget.userWidget),
onLongPress: widget.onUserLongPress,
),
SizedBox(height: 4),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
user.name,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
),
],
),
);
},
);
} else {
return _buildQueryProgressIndicator(context, usersProvider);
}
}
Widget _buildQueryProgressIndicator(context, UsersBlocState usersProvider) {
return StreamBuilder<bool>(
stream: usersProvider.queryUsersLoading,
initialData: false,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Container(
color: StreamChatTheme.of(context)
.colorTheme
.accentRed
.withOpacity(.2),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Center(
child: Text('Error loading users'),
),
),
);
}
return Container(
height: 100,
padding: EdgeInsets.all(32),
child: Center(
child: snapshot.data ? CircularProgressIndicator() : Container(),
),
);
});
}
Widget _separatorBuilder(context, i) {
return Container(
height: 1,
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
);
}
void _listenUserPagination(UsersBlocState usersProvider) {
usersProvider.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination.copyWith(
offset: usersProvider.users?.length ?? 0,
),
options: widget.options,
);
}
@override
void didUpdateWidget(UserListView oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.filter?.toString() != oldWidget.filter?.toString() ||
jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) ||
widget.pagination?.toJson()?.toString() !=
oldWidget.pagination?.toJson()?.toString() ||
widget.options?.toString() != oldWidget.options?.toString()) {
final usersBloc = UsersBloc.of(context);
usersBloc.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination,
options: widget.options,
);
}
}
}
abstract class ListItem {
String get key {
if (this is ListHeaderItem) {
final header = (this as ListHeaderItem).heading;
return 'HEADER-$header';
}
if (this is ListUserItem) {
final user = (this as ListUserItem).user;
return 'USER-${user.id}';
}
}
Widget when({
@required Widget Function(String heading) headerItem,
@required Widget Function(User user) userItem,
}) {
if (this is ListHeaderItem) {
return headerItem((this as ListHeaderItem).heading);
}
if (this is ListUserItem) {
return userItem((this as ListUserItem).user);
}
}
}
class ListHeaderItem extends ListItem {
final String heading;
ListHeaderItem(this.heading);
}
class ListUserItem extends ListItem {
final User user;
ListUserItem(this.user);
}
-55
View File
@@ -1,55 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/user_avatar.dart';
class UserReactionDisplay extends StatelessWidget {
const UserReactionDisplay({
Key key,
@required this.reactionToEmoji,
@required this.message,
this.size = 30,
}) : super(key: key);
final Map<String, String> reactionToEmoji;
final Message message;
final double size;
@override
Widget build(BuildContext context) {
return Container(
color: Colors.black87,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: reactionToEmoji.keys.map((reactionType) {
var firstUserReaction = message.latestReactions.firstWhere(
(element) => element.type == reactionType, orElse: () {
return null;
});
if (firstUserReaction == null) {
return IconButton(
iconSize: size,
icon: Container(),
onPressed: null,
);
}
return IconButton(
iconSize: size,
icon: UserAvatar(
user: firstUserReaction.user,
constraints: BoxConstraints(
maxHeight: size - 5,
maxWidth: size - 5,
),
onTap: (user) {},
),
onPressed: () {},
);
}).toList(),
),
);
}
}
-108
View File
@@ -1,108 +0,0 @@
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/stream_chat.dart';
import 'stream_chat.dart';
/// Widget dedicated to the management of a users list with pagination
class UsersBloc extends StatefulWidget {
/// The widget child
final Widget child;
/// Instantiate a new UsersBloc
const UsersBloc({
Key key,
@required this.child,
}) : super(key: key);
@override
UsersBlocState createState() => UsersBlocState();
/// Use this method to get the current [UsersBlocState] instance
static UsersBlocState of(BuildContext context) {
UsersBlocState state;
state = context.findAncestorStateOfType<UsersBlocState>();
if (state == null) {
throw Exception('You must have a UsersBloc widget as ancestor');
}
return state;
}
}
/// The current state of the [UsersBloc]
class UsersBlocState extends State<UsersBloc>
with AutomaticKeepAliveClientMixin {
/// The current users list
List<User> get users => _usersController.value;
/// The current users list as a stream
Stream<List<User>> get usersStream => _usersController.stream;
final BehaviorSubject<List<User>> _usersController = BehaviorSubject();
final BehaviorSubject<bool> _queryUsersLoadingController =
BehaviorSubject.seeded(false);
/// The stream notifying the state of queryUsers call
Stream<bool> get queryUsersLoading => _queryUsersLoadingController.stream;
/// Calls [Client.queryUsers] updating [queryUsersLoading] stream
Future<void> queryUsers({
Map<String, dynamic> filter,
List<SortOption> sort,
Map<String, dynamic> options,
PaginationParams pagination,
}) async {
final client = StreamChat.of(context).client;
if (client.state?.user == null ||
_queryUsersLoadingController.value == true) {
return;
}
_queryUsersLoadingController.add(true);
try {
final clear = pagination == null ||
pagination.offset == null ||
pagination.offset == 0;
final oldUsers = List<User>.from(users ?? []);
final usersResponse = await client.queryUsers(
filter: filter,
sort: sort,
options: options,
pagination: pagination,
);
if (clear) {
_usersController.add(usersResponse.users);
} else {
final temp = oldUsers + usersResponse.users;
_usersController.add(temp);
}
_queryUsersLoadingController.add(false);
} catch (err, stackTrace) {
_queryUsersLoadingController.addError(err, stackTrace);
}
}
@override
Widget build(BuildContext context) {
super.build(context);
return widget.child;
}
@override
void dispose() {
_usersController.close();
_queryUsersLoadingController.close();
super.dispose();
}
@override
bool get wantKeepAlive => true;
}
-217
View File
@@ -1,217 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:url_launcher/url_launcher.dart';
import 'stream_svg_icon.dart';
import '../stream_chat_flutter.dart';
Future<void> launchURL(BuildContext context, String url) async {
if (await canLaunch(url)) {
await launch(url);
} else {
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('Cannot launch the url'),
),
);
}
}
Future<bool> showConfirmationDialog(
BuildContext context, {
String title,
Widget icon,
String question,
String okText,
String cancelText,
}) {
return showModalBottomSheet(
backgroundColor: StreamChatTheme.of(context).colorTheme.white,
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,
),
if (icon != null) icon,
SizedBox(
height: 26.0,
),
Text(
title,
style: StreamChatTheme.of(context).textTheme.headlineBold,
),
SizedBox(
height: 7.0,
),
Text(question),
SizedBox(
height: 36.0,
),
Container(
color:
StreamChatTheme.of(context).colorTheme.black.withOpacity(.08),
height: 1.0,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
FlatButton(
child: Text(
cancelText,
style: TextStyle(
color: StreamChatTheme.of(context)
.colorTheme
.black
.withOpacity(0.5),
fontWeight: FontWeight.w400),
),
onPressed: () {
Navigator.of(context).pop();
},
),
FlatButton(
child: Text(
okText,
style: TextStyle(
color: Colors.red, fontWeight: FontWeight.w400),
),
onPressed: () {
Navigator.pop(context, true);
},
),
],
),
],
);
});
}
/// Get random png with initials
String getRandomPicUrl(User user) =>
'https://getstream.io/random_png/?id=${user.id}&name=${user.name}';
/// Get websiteName from [hostName]
String getWebsiteName(String hostName) {
switch (hostName) {
case 'reddit':
return 'Reddit';
case 'youtube':
return 'Youtube';
case 'wikipedia':
return 'Wikipedia';
case 'twitter':
return 'Twitter';
case 'facebook':
return 'Facebook';
case 'amazon':
return 'Amazon';
case 'yelp':
return 'Yelp';
case 'imdb':
return 'IMDB';
case 'pinterest':
return 'Pinterest';
case 'tripadvisor':
return 'TripAdvisor';
case 'instagram':
return 'Instagram';
case 'walmart':
return 'Walmart';
case 'craigslist':
return 'Craigslist';
case 'ebay':
return 'eBay';
case 'linkedin':
return 'LinkedIn';
case 'google':
return 'Google';
case 'apple':
return 'Apple';
default:
return null;
}
}
///
String getSizeText(int bytes) {
if (bytes == null) {
return 'Size N/A';
}
if (bytes <= 1000) {
return '${bytes} bytes';
} else if (bytes <= 100000) {
return '${(bytes / 1000).toStringAsFixed(2)} KB';
} else {
return '${(bytes / 1000000).toStringAsFixed(2)} MB';
}
}
///
StreamSvgIcon getFileTypeImage(String type) {
switch (type) {
case '7z':
return StreamSvgIcon.filetype_7z();
break;
case 'csv':
return StreamSvgIcon.filetype_csv();
break;
case 'doc':
return StreamSvgIcon.filetype_doc();
break;
case 'docx':
return StreamSvgIcon.filetype_docx();
break;
case 'html':
return StreamSvgIcon.filetype_html();
break;
case 'md':
return StreamSvgIcon.filetype_md();
break;
case 'odt':
return StreamSvgIcon.filetype_odt();
break;
case 'pdf':
return StreamSvgIcon.filetype_pdf();
break;
case 'ppt':
return StreamSvgIcon.filetype_ppt();
break;
case 'pptx':
return StreamSvgIcon.filetype_pptx();
break;
case 'rar':
return StreamSvgIcon.filetype_rar();
break;
case 'rtf':
return StreamSvgIcon.filetype_rtf();
break;
case 'tar':
return StreamSvgIcon.filetype_tar();
break;
case 'txt':
return StreamSvgIcon.filetype_txt();
break;
case 'xls':
return StreamSvgIcon.filetype_xls();
break;
case 'xlsx':
return StreamSvgIcon.filetype_xlsx();
break;
case 'zip':
return StreamSvgIcon.filetype_zip();
break;
default:
return StreamSvgIcon.filetype_Generic();
break;
}
}
-169
View File
@@ -1,169 +0,0 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/full_screen_media.dart';
import 'package:stream_chat_flutter/src/utils.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart';
import 'attachment_error.dart';
import 'attachment_title.dart';
class VideoAttachment extends StatefulWidget {
final Attachment attachment;
final MessageTheme messageTheme;
final Size size;
final Message message;
final ShowMessageCallback onShowMessage;
VideoAttachment({
Key key,
@required this.attachment,
@required this.messageTheme,
this.message,
this.size,
this.onShowMessage,
}) : super(key: key);
@override
_VideoAttachmentState createState() => _VideoAttachmentState();
}
class _VideoAttachmentState extends State<VideoAttachment> {
ChewieController _chewieController;
VideoPlayerController _videoPlayerController;
bool initialized = false;
@override
Widget build(BuildContext context) {
if (!initialized) {
return Container(
height: widget.size?.height ?? 100,
width: widget.size?.width ?? 100,
child: Center(
child: CircularProgressIndicator(),
),
);
}
_chewieController = ChewieController(
videoPlayerController: _videoPlayerController,
autoInitialize: true,
showControls: false,
aspectRatio: _videoPlayerController.value.aspectRatio,
errorBuilder: (_, e) {
if (widget.attachment.thumbUrl != null) {
return Stack(
children: <Widget>[
Container(
height: widget.size?.height,
width: widget.size?.width,
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
image: CachedNetworkImageProvider(
widget.attachment.thumbUrl,
),
),
),
),
if (widget.attachment.titleLink != null)
Material(
color: Colors.transparent,
child: InkWell(
onTap: () =>
launchURL(context, widget.attachment.titleLink),
),
),
],
);
}
return AttachmentError(
attachment: widget.attachment,
size: widget.size,
);
});
return GestureDetector(
onTap: () {
final channel = StreamChannel.of(context).channel;
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => StreamChannel(
channel: channel,
child: FullScreenMedia(
mediaAttachments: [widget.attachment],
userName: widget.message.user.name,
sentAt: widget.message.createdAt,
message: widget.message,
onShowMessage: widget.onShowMessage,
),
),
),
);
},
child: Container(
height: widget.size?.height,
width: widget.size?.width,
child: Flex(
direction: Axis.vertical,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Expanded(
child: FittedBox(
fit: BoxFit.none,
child: Stack(
children: <Widget>[
Chewie(
controller: _chewieController,
),
Positioned.fill(
child: Center(
child: Material(
shape: CircleBorder(),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Icon(Icons.play_arrow),
),
),
),
),
],
),
),
),
if (widget.attachment.title != null)
Material(
color: widget.messageTheme.messageBackgroundColor,
child: AttachmentTitle(
messageTheme: widget.messageTheme,
attachment: widget.attachment,
),
),
],
),
),
);
}
@override
void initState() {
super.initState();
_videoPlayerController =
VideoPlayerController.network(widget.attachment.assetUrl);
_videoPlayerController.initialize().whenComplete(() {
setState(() {
initialized = true;
});
});
}
@override
void dispose() {
_videoPlayerController?.dispose();
_chewieController?.dispose();
super.dispose();
}
}
-48
View File
@@ -1,48 +0,0 @@
export 'package:stream_chat/stream_chat.dart';
export 'src/back_button.dart';
export 'src/channel_header.dart';
export 'src/channel_image.dart';
export 'src/channel_list_header.dart';
export 'src/channel_list_view.dart';
export 'src/channel_name.dart';
export 'src/channel_preview.dart';
export 'src/channels_bloc.dart';
export 'src/date_divider.dart';
export 'src/deleted_message.dart';
export 'src/file_attachment.dart';
export 'src/full_screen_media.dart';
export 'src/image_header.dart';
export 'src/image_footer.dart';
export 'src/giphy_attachment.dart';
export 'src/image_attachment.dart';
export 'src/message_input.dart';
export 'src/message_list_view.dart';
export 'src/message_text.dart';
export 'src/message_widget.dart';
export 'src/reaction_picker.dart';
export 'src/sending_indicator.dart';
export 'src/stream_channel.dart';
export 'src/stream_chat.dart';
export 'src/stream_chat_theme.dart';
export 'src/stream_neumorphic_button.dart';
export 'src/stream_svg_icon.dart';
export 'src/system_message.dart';
export 'src/thread_header.dart';
export 'src/typing_indicator.dart';
export 'src/user_avatar.dart';
export 'src/user_item.dart';
export 'src/user_item.dart';
export 'src/user_list_view.dart';
export 'src/user_list_view.dart';
export 'src/users_bloc.dart';
export 'src/users_bloc.dart';
export 'src/utils.dart';
export 'src/video_attachment.dart';
export 'src/message_search_bloc.dart';
export 'src/message_search_item.dart';
export 'src/message_search_list_view.dart';
export 'src/unread_indicator.dart';
export 'src/option_list_tile.dart';
export 'src/channel_file_display_screen.dart';
export 'src/channel_media_display_screen.dart';
-3
View File
@@ -1,3 +0,0 @@
<svg width="136" height="136" viewBox="0 0 136 136" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M84.9999 17H50.9999L39.6666 28.3333H16.9999C13.8703 28.3333 11.3333 30.8704 11.3333 34V113.333C11.3333 116.463 13.8703 119 16.9999 119H119C122.13 119 124.667 116.463 124.667 113.333V34C124.667 30.8704 122.13 28.3333 119 28.3333H96.3333L84.9999 17ZM44.3586 39.6667L55.6918 28.3333H80.3079L91.6413 39.6667H113.333V107.667H22.6666V39.6667H44.3586ZM67.9999 102C50.787 102 36.8333 88.0462 36.8333 70.8333C36.8333 53.6204 50.787 39.6667 67.9999 39.6667C85.2128 39.6667 99.1666 53.6204 99.1666 70.8333C99.1666 88.0462 85.2128 102 67.9999 102ZM87.8333 70.8333C87.8333 81.787 78.9536 90.6667 67.9999 90.6667C57.0463 90.6667 48.1665 81.787 48.1665 70.8333C48.1665 59.8797 57.0463 51 67.9999 51C78.9536 51 87.8333 59.8797 87.8333 70.8333Z" fill="#DBDBDB"/>
</svg>

Before

Width:  |  Height:  |  Size: 902 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="136" height="136" viewBox="0 0 136 136" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M119 28.3333H70.3459L59.0126 17H16.9999C13.8703 17 11.3333 19.5371 11.3333 22.6667V113.333C11.3333 116.463 13.8703 119 16.9999 119H119C122.13 119 124.667 116.463 124.667 113.333V34C124.667 30.8704 122.13 28.3333 119 28.3333ZM22.6666 107.667V39.6667H113.333V107.667H22.6666Z" fill="#D6D6D6"/>
</svg>

Before

Width:  |  Height:  |  Size: 448 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="136" height="136" viewBox="0 0 136 136" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M68 28.3333C42.3172 28.3333 22.6666 46.7017 22.6666 68C22.6666 89.2982 42.3172 107.667 68 107.667C71.5184 107.667 74.936 107.316 78.213 106.655C78.8975 106.517 79.6013 106.507 80.2893 106.625L110.112 111.73L104.192 96.428C103.462 94.5387 103.795 92.4029 105.067 90.8265C110.315 84.3194 113.333 76.4456 113.333 68C113.333 46.7017 93.683 28.3333 68 28.3333ZM11.3333 68C11.3333 39.2244 37.3497 17 68 17C98.6504 17 124.667 39.2244 124.667 68C124.667 78.0719 121.423 87.4265 115.899 95.2754L124.285 116.955C125.021 118.858 124.677 121.009 123.386 122.588C122.095 124.167 120.055 124.93 118.044 124.586L79.3922 117.969C75.7072 118.646 71.8969 119 68 119C37.3497 119 11.3333 96.7753 11.3333 68Z" fill="#DBDBDB"/>
</svg>

Before

Width:  |  Height:  |  Size: 862 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="136" height="136" viewBox="0 0 136 136" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M119.382 119.665C121.592 117.453 121.592 113.868 119.38 111.657L92.9937 85.27C99.244 77.3253 102.667 67.6013 102.667 57.3333C102.667 45.2237 97.9463 33.8393 89.384 25.2827C80.8273 16.7147 69.443 12 57.3333 12C45.2237 12 33.8393 16.7147 25.2827 25.2827C16.7203 33.8393 12 45.2237 12 57.3333C12 69.443 16.7203 80.8273 25.2827 89.3897C33.8393 97.952 45.2237 102.667 57.3333 102.667C67.4597 102.667 77.0533 99.329 84.9413 93.2373L111.371 119.667C113.583 121.879 117.17 121.878 119.382 119.665ZM33.2953 81.377C26.8693 74.9567 23.3333 66.417 23.3333 57.3333C23.3333 48.244 26.8693 39.7157 33.2953 33.2953C39.7157 26.8693 48.2497 23.3333 57.3333 23.3333C66.417 23.3333 74.951 26.8693 81.3713 33.301C87.7973 39.7157 91.3333 48.244 91.3333 57.3333C91.3333 66.417 87.7973 74.9567 81.3713 81.377C74.951 87.7973 66.417 91.3333 57.3333 91.3333C48.2497 91.3333 39.7157 87.7973 33.2953 81.377Z" fill="#DBDBDB"/>
</svg>

Before

Width:  |  Height:  |  Size: 1013 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="136" height="136" viewBox="0 0 136 136" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M28.3333 28.3335C18.9444 28.3335 11.3333 35.9447 11.3333 45.3335V90.6669C11.3333 100.055 18.9444 107.667 28.3333 107.667H107.667C117.055 107.667 124.667 100.055 124.667 90.6669V45.3335C124.667 35.9447 117.055 28.3335 107.667 28.3335H28.3333ZM22.6666 45.3335C22.6666 42.2039 25.2036 39.6668 28.3333 39.6668H107.667C110.796 39.6668 113.333 42.2039 113.333 45.3335V75.0149L95.0199 53.039C93.9306 51.7318 92.3115 50.9833 90.61 51.0003C88.9085 51.0173 87.3047 51.798 86.2417 53.1267L67.0061 77.1713L48.7333 63.4667C46.4773 61.7747 43.3204 61.999 41.3264 63.9931L22.6666 82.6528V45.3335ZM25.5002 95.5756C26.3336 96.0576 27.3012 96.3335 28.3333 96.3335H107.667C110.193 96.3335 112.334 94.6797 113.064 92.3954L90.7563 65.6258L72.4249 88.5399C70.5129 90.9299 67.0486 91.3698 64.6 89.5333L45.8651 75.4821L26.6736 94.6736C26.3147 95.0325 25.9197 95.3331 25.5002 95.5756Z" fill="#D6D6D6"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

-8
View File
@@ -1,8 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 24C18.627 24 24 18.627 24 12C24 5.373 18.627 0 12 0C5.373 0 0 5.373 0 12C0 18.627 5.373 24 12 24Z" fill="black"/>
<path d="M7 7H8V17H7V7Z" fill="#00FF99"/>
<path d="M16 10H17V18H16V10Z" fill="#9D34FF"/>
<path d="M7 6H14V7H7V6Z" fill="#FFFF9C"/>
<path d="M7 17H16V18H7V17Z" fill="#00CCFF"/>
<path d="M14 6H15V7.5H16V9H17V10H14V6Z" fill="#FF6666"/>
</svg>

Before

Width:  |  Height:  |  Size: 463 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.677 21.168L5.897 21.521C4.029 22.365 3.07 22.562 3.02 22.111C2.962 21.588 2.937 19.657 2.946 16.317C2.956 14.101 2.974 11.895 2.998 9.69796C3.044 9.40796 3.407 9.11096 4.086 8.80396C4.754 8.50096 5.18 8.41796 5.362 8.55296C5.412 8.60696 5.453 8.77296 5.484 9.05296C5.608 10.173 5.638 13.055 5.574 17.698C5.564 18.214 5.56 18.688 5.564 19.121L8.405 17.76C8.684 17.66 8.887 17.737 9.016 17.992C9.145 18.247 9.22 18.478 9.243 18.685C9.326 19.427 9.109 19.935 8.593 20.206L8.069 20.462C7.564 20.729 7.099 20.964 6.677 21.168ZM12.127 18.762C10.868 19.267 10.02 19.078 9.582 18.196C9.144 17.314 8.863 16.313 8.739 15.193C8.682 14.683 8.649 14.123 8.637 13.514C8.581 11.609 8.787 9.89896 9.255 8.38396C9.722 6.86896 10.582 5.82896 11.835 5.26296L11.866 5.24796C13.128 4.76796 13.979 4.95796 14.42 5.81896C14.861 6.68096 15.142 7.65296 15.262 8.73596C15.322 9.27196 15.352 9.85196 15.354 10.477C15.482 14.944 14.406 17.706 12.127 18.762ZM12.051 16.112C12.661 15.785 12.935 14.242 12.875 11.483C12.8684 10.8526 12.832 10.2229 12.766 9.59596C12.7136 9.16803 12.6443 8.74236 12.558 8.31996C12.455 7.79696 12.23 7.57596 11.882 7.65696C11.542 7.80996 11.311 8.33396 11.187 9.22696C11.058 10.2248 11.0079 11.2313 11.037 12.237C11.048 12.948 11.085 13.583 11.147 14.143C11.185 14.484 11.257 14.917 11.367 15.443C11.475 15.97 11.703 16.193 12.051 16.113V16.112ZM20 15.145L19.22 15.497C17.353 16.342 16.394 16.539 16.344 16.088C16.286 15.565 16.262 13.633 16.27 10.294C16.28 8.07796 16.298 5.87096 16.322 3.67396C16.368 3.38596 16.731 3.08796 17.41 2.77996C18.078 2.47796 18.504 2.39496 18.686 2.52996C18.736 2.58296 18.777 2.74996 18.808 3.02996C18.933 4.14996 18.962 7.03196 18.898 11.675C18.888 12.191 18.884 12.665 18.888 13.098L21.729 11.737C22.008 11.637 22.212 11.714 22.34 11.969C22.469 12.224 22.544 12.455 22.567 12.662C22.65 13.404 22.433 13.912 21.917 14.182L21.394 14.438C20.888 14.705 20.424 14.941 20.001 15.145H20Z" fill="#808080"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 5C3.73478 5 3.48043 5.10536 3.29289 5.29289C3.10536 5.48043 3 5.73478 3 6C3 6.26522 3.10536 6.51957 3.29289 6.70711C3.48043 6.89464 3.73478 7 4 7H20C20.2652 7 20.5196 6.89464 20.7071 6.70711C20.8946 6.51957 21 6.26522 21 6C21 5.73478 20.8946 5.48043 20.7071 5.29289C20.5196 5.10536 20.2652 5 20 5H4ZM3 12C3 11.7348 3.10536 11.4804 3.29289 11.2929C3.48043 11.1054 3.73478 11 4 11H13C13.2652 11 13.5196 11.1054 13.7071 11.2929C13.8946 11.4804 14 11.7348 14 12C14 12.2652 13.8946 12.5196 13.7071 12.7071C13.5196 12.8946 13.2652 13 13 13H4C3.73478 13 3.48043 12.8946 3.29289 12.7071C3.10536 12.5196 3 12.2652 3 12ZM10 18C10 17.7348 10.1054 17.4804 10.2929 17.2929C10.4804 17.1054 10.7348 17 11 17H20C20.2652 17 20.5196 17.1054 20.7071 17.2929C20.8946 17.4804 21 17.7348 21 18C21 18.2652 20.8946 18.5196 20.7071 18.7071C20.5196 18.8946 20.2652 19 20 19H11C10.7348 19 10.4804 18.8946 10.2929 18.7071C10.1054 18.5196 10 18.2652 10 18Z" fill="#808080"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 7C16 8.31528 15.3652 9.48229 14.3852 10.2113C15.3987 10.3984 16.318 10.7031 17.1402 11.0991C17.6378 11.3387 17.8469 11.9363 17.6072 12.4339C17.3676 12.9315 16.7699 13.1406 16.2724 12.9009C15.1351 12.3532 13.716 12 12 12C6.4686 12 4 15.6324 4 18C4 18.5523 3.55228 19 3 19C2.44771 19 2 18.5523 2 18C2 14.8422 4.66883 11.1288 9.61611 10.2123C8.63539 9.48334 8 8.31587 8 7C8 4.79086 9.79086 3 12 3C14.2091 3 16 4.79086 16 7ZM12 9C13.1046 9 14 8.10457 14 7C14 5.89543 13.1046 5 12 5C10.8954 5 10 5.89543 10 7C10 8.10457 10.8954 9 12 9ZM18 14H20V16H22V18H20V20H18V18H16V16H18V14Z" fill="#808080"/>
</svg>

Before

Width:  |  Height:  |  Size: 748 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.9113 7C16.9113 8.31528 16.2783 9.4823 15.301 10.2113C16.3117 10.3984 17.2284 10.7031 18.0483 11.0991C18.5445 11.3387 18.753 11.9363 18.514 12.4339C18.2751 12.9315 17.6791 13.1406 17.1829 12.9009C16.0488 12.3532 14.6336 12 12.9223 12C7.4063 12 4.94453 15.6324 4.94453 18C4.94453 18.5523 4.49806 19 3.9473 19C3.39655 19 2.95007 18.5523 2.95007 18C2.95007 14.8422 5.61152 11.1288 10.5451 10.2123C9.56709 9.48333 8.93347 8.31587 8.93347 7C8.93347 4.79086 10.7193 3 12.9224 3C15.1254 3 16.9113 4.79086 16.9113 7ZM12.9224 9C14.0239 9 14.9168 8.10457 14.9168 7C14.9168 5.89543 14.0239 5 12.9224 5C11.8208 5 10.9279 5.89543 10.9279 7C10.9279 8.10457 11.8208 9 12.9224 9ZM22.8948 18V16H16.9114V18H22.8948Z" fill="#808080"/>
</svg>

Before

Width:  |  Height:  |  Size: 871 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.2929 7.29289C15.6834 6.90237 16.3166 6.90237 16.7071 7.29289L20.7011 11.287C20.7168 11.3024 20.7319 11.3183 20.7466 11.3347C20.7814 11.3736 20.8126 11.4147 20.8402 11.4574C20.8626 11.492 20.8827 11.5278 20.9004 11.5644C20.9026 11.5689 20.9047 11.5733 20.9068 11.5778C20.9666 11.7061 21 11.8491 21 12C21 12.1561 20.9642 12.3039 20.9004 12.4356C20.8676 12.5034 20.8267 12.5682 20.7777 12.6287C20.7738 12.6335 20.7698 12.6383 20.7658 12.6431C20.745 12.6679 20.723 12.6916 20.6999 12.7143L16.7071 16.7071C16.3166 17.0976 15.6834 17.0976 15.2929 16.7071C14.9024 16.3166 14.9024 15.6834 15.2929 15.2929L17.5858 13H4C3.44772 13 3 12.5523 3 12C3 11.4477 3.44772 11 4 11H17.5858L15.2929 8.70711C14.9024 8.31658 14.9024 7.68342 15.2929 7.29289Z" fill="#006CFF"/>
</svg>

Before

Width:  |  Height:  |  Size: 909 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.468 2.88798C14.5039 2.78848 15.5454 3.01018 16.451 3.52298C17.3476 4.05069 18.0602 4.84146 18.492 5.78798C18.942 6.79798 19.038 8.04198 18.348 9.23698L13.848 17.031C13.5149 17.5973 13.0171 18.0487 12.421 18.325C11.664 18.667 10.725 18.692 9.74998 18.129C8.77498 17.566 8.32798 16.74 8.24398 15.914C8.18545 15.2594 8.32777 14.6026 8.65198 14.031L12.652 7.10298C12.7855 6.87518 13.0038 6.70944 13.2591 6.64195C13.5144 6.57445 13.786 6.61067 14.0147 6.74271C14.2434 6.87475 14.4106 7.09188 14.4797 7.34672C14.5489 7.60156 14.5145 7.87343 14.384 8.10298L10.384 15.031C10.297 15.181 10.208 15.456 10.234 15.713C10.254 15.909 10.339 16.16 10.75 16.397C11.16 16.634 11.421 16.582 11.6 16.502C11.836 16.395 12.03 16.182 12.116 16.032L16.616 8.23698C16.926 7.69898 16.906 7.14398 16.665 6.60098C16.4049 6.04043 15.9818 5.57134 15.451 5.25498C14.9115 4.9535 14.2936 4.82176 13.678 4.87698C13.087 4.93898 12.596 5.19898 12.286 5.73698L7.28598 14.397C6.63198 15.53 6.99998 17.697 9.24998 18.995C11.5 20.295 13.56 19.53 14.214 18.397L17.714 12.335C17.7792 12.2203 17.8664 12.1197 17.9706 12.0389C18.0748 11.9581 18.194 11.8987 18.3213 11.8642C18.4486 11.8296 18.5814 11.8206 18.7122 11.8376C18.843 11.8546 18.9691 11.8973 19.0833 11.9632C19.1975 12.0292 19.2976 12.1171 19.3777 12.2218C19.4578 12.3266 19.5164 12.4461 19.5501 12.5736C19.5838 12.7011 19.592 12.8341 19.5741 12.9647C19.5562 13.0954 19.5127 13.2212 19.446 13.335L15.946 19.397C14.601 21.727 11.196 22.428 8.24998 20.727C5.30398 19.027 4.20798 15.727 5.55398 13.397L10.554 4.73698C11.244 3.54198 12.369 3.00398 13.468 2.88698V2.88798Z" fill="#808080"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M21 9H15L13 11H9C8.44771 11 8 11.4477 8 12V26C8 26.5523 8.44771 27 9 27H27C27.5523 27 28 26.5523 28 26V12C28 11.4477 27.5523 11 27 11H23L21 9ZM13.828 13L15.828 11H20.172L22.172 13H26V25H10V13H13.828ZM18 24C14.9624 24 12.5 21.5376 12.5 18.5C12.5 15.4624 14.9624 13 18 13C21.0376 13 23.5 15.4624 23.5 18.5C23.5 21.5376 21.0376 24 18 24ZM21.5 18.5C21.5 20.433 19.933 22 18 22C16.067 22 14.5 20.433 14.5 18.5C14.5 16.567 16.067 15 18 15C19.933 15 21.5 16.567 21.5 18.5Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 634 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.471 5.80499C12.731 5.54399 12.731 5.12199 12.471 4.86199C12.211 4.60199 11.789 4.60199 11.529 4.86199L6.66648 9.72359L4.47105 7.52898C4.21105 7.26898 3.78905 7.26898 3.52905 7.52898H3.53005C3.40536 7.65401 3.33533 7.82339 3.33533 7.99998C3.33533 8.17657 3.40536 8.34595 3.53005 8.47098L6.19505 11.138C6.2564 11.2024 6.33002 11.2539 6.41158 11.2895C6.49313 11.325 6.58097 11.3439 6.66992 11.345C6.75887 11.346 6.84715 11.3293 6.92954 11.2958C7.01193 11.2622 7.08679 11.2125 7.14969 11.1496C7.18485 11.1144 7.21588 11.0756 7.24227 11.0337L12.471 5.80499Z" fill="#808080"/>
</svg>

Before

Width:  |  Height:  |  Size: 727 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.3 4.86177C11.5611 5.12212 11.5611 5.54423 11.3 5.80458L8.43002 8.66658L9.4902 9.72383L14.3658 4.86177C14.6269 4.60142 15.0502 4.60142 15.3112 4.86177C15.5723 5.12212 15.5723 5.54423 15.3112 5.80458L9.97014 11.1309C9.96777 11.1333 9.96538 11.1357 9.96297 11.1381C9.70189 11.3984 9.27861 11.3984 9.01753 11.1381L7.48459 9.60939L5.96419 11.1256L5.95181 11.1382C5.69073 11.3985 5.26745 11.3985 5.00637 11.1382L2.33228 8.47156C2.0712 8.21121 2.0712 7.7891 2.33228 7.52876C2.59335 7.26841 3.01664 7.26841 3.27771 7.52876L5.47893 9.72388L10.3546 4.86177C10.6156 4.60142 11.0389 4.60142 11.3 4.86177Z" fill="#006CFF"/>
</svg>

Before

Width:  |  Height:  |  Size: 767 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.9998 7.99979C18.9998 7.73462 18.8945 7.48031 18.707 7.29279C18.5195 7.10532 18.2652 7 18 7C17.7349 7 17.4806 7.10532 17.293 7.29279L10 14.5858L6.70703 11.2928C6.5195 11.1053 6.26519 11 6.00003 11C5.73487 11 5.48056 11.1053 5.29303 11.2928C5.10556 11.4803 5.00024 11.7346 5.00024 11.9998C5.00024 12.265 5.10556 12.5193 5.29303 12.7068L9.29303 16.7068C9.48163 16.8889 9.73423 16.9897 9.99643 16.9875C9.99763 16.9875 9.99883 16.9874 10 16.9874C10.0012 16.9874 10.0024 16.9875 10.0036 16.9875C10.2658 16.9897 10.5184 16.8889 10.707 16.7068L18.707 8.70679C18.8945 8.51926 18.9998 8.26495 18.9998 7.99979Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 772 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.477 2 2 6.477 2 12C2 17.523 6.477 22 12 22C17.523 22 22 17.523 22 12C22 6.477 17.523 2 12 2ZM15.707 9.293C15.8945 9.48053 15.9998 9.73484 15.9998 10C15.9998 10.2652 15.8945 10.5195 15.707 10.707L11.707 14.707C11.5195 14.8945 11.2652 14.9998 11 14.9998C10.7348 14.9998 10.4805 14.8945 10.293 14.707L8.293 12.707C8.19749 12.6148 8.12131 12.5044 8.0689 12.3824C8.01649 12.2604 7.9889 12.1292 7.98775 11.9964C7.9866 11.8636 8.0119 11.7319 8.06218 11.609C8.11246 11.4861 8.18671 11.3745 8.28061 11.2806C8.3745 11.1867 8.48615 11.1125 8.60905 11.0622C8.73194 11.0119 8.86362 10.9866 8.9964 10.9877C9.12918 10.9889 9.2604 11.0165 9.3824 11.0689C9.50441 11.1213 9.61475 11.1975 9.707 11.293L11 12.586L14.293 9.293C14.4805 9.10553 14.7348 9.00021 15 9.00021C15.2652 9.00021 15.5195 9.10553 15.707 9.293V9.293Z" fill="#006CFF"/>
</svg>

Before

Width:  |  Height:  |  Size: 979 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M22 12C22 6.48 17.52 2 12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12ZM8 11H12V8L16 12L12 16V13H8V11Z" fill="#808080"/>
</svg>

Before

Width:  |  Height:  |  Size: 295 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM11 16V12H8L12 8L16 12H13V16H11Z" fill="#006BFE"/>
</svg>

Before

Width:  |  Height:  |  Size: 295 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.05004 7.05011C6.86257 7.23764 6.75725 7.49195 6.75725 7.75711C6.75725 8.02227 6.86257 8.27658 7.05004 8.46411L10.586 12.0001L7.05004 15.5361C6.95453 15.6284 6.87835 15.7387 6.82594 15.8607C6.77353 15.9827 6.74594 16.1139 6.74479 16.2467C6.74363 16.3795 6.76894 16.5112 6.81922 16.6341C6.8695 16.757 6.94375 16.8686 7.03764 16.9625C7.13154 17.0564 7.24319 17.1307 7.36609 17.1809C7.48898 17.2312 7.62066 17.2565 7.75344 17.2554C7.88622 17.2542 8.01744 17.2266 8.13944 17.1742C8.26145 17.1218 8.37179 17.0456 8.46404 16.9501L12 13.4141L15.536 16.9501C15.7246 17.1323 15.9772 17.2331 16.2394 17.2308C16.5016 17.2285 16.7524 17.1233 16.9379 16.9379C17.1233 16.7525 17.2284 16.5017 17.2307 16.2395C17.233 15.9773 17.1322 15.7247 16.95 15.5361L13.414 12.0001L16.95 8.46411C17.1322 8.27551 17.233 8.02291 17.2307 7.76071C17.2284 7.49851 17.1233 7.2477 16.9379 7.06229C16.7524 6.87688 16.5016 6.77171 16.2394 6.76944C15.9772 6.76716 15.7246 6.86795 15.536 7.05011L12 10.5861L8.46404 7.05011C8.27651 6.86264 8.0222 6.75732 7.75704 6.75732C7.49188 6.75732 7.23757 6.86264 7.05004 7.05011V7.05011Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.05004 7.05011C6.86257 7.23764 6.75725 7.49195 6.75725 7.75711C6.75725 8.02227 6.86257 8.27658 7.05004 8.46411L10.586 12.0001L7.05004 15.5361C6.95453 15.6284 6.87835 15.7387 6.82594 15.8607C6.77353 15.9827 6.74594 16.1139 6.74479 16.2467C6.74363 16.3795 6.76894 16.5112 6.81922 16.6341C6.8695 16.757 6.94375 16.8686 7.03764 16.9625C7.13154 17.0564 7.24319 17.1307 7.36609 17.1809C7.48898 17.2312 7.62066 17.2565 7.75344 17.2554C7.88622 17.2542 8.01744 17.2266 8.13944 17.1742C8.26145 17.1218 8.37179 17.0456 8.46404 16.9501L12 13.4141L15.536 16.9501C15.7246 17.1323 15.9772 17.2331 16.2394 17.2308C16.5016 17.2285 16.7524 17.1233 16.9379 16.9379C17.1233 16.7525 17.2284 16.5017 17.2307 16.2395C17.233 15.9773 17.1322 15.7247 16.95 15.5361L13.414 12.0001L16.95 8.46411C17.1322 8.27551 17.233 8.02291 17.2307 7.76071C17.2284 7.49851 17.1233 7.2477 16.9379 7.06229C16.7524 6.87688 16.5016 6.77171 16.2394 6.76944C15.9772 6.76716 15.7246 6.86795 15.536 7.05011L12 10.5861L8.46404 7.05011C8.27651 6.86264 8.0222 6.75732 7.75704 6.75732C7.49188 6.75732 7.23757 6.86264 7.05004 7.05011V7.05011Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 12C2 6.477 6.477 2 12 2C17.523 2 22 6.477 22 12C22 17.523 17.523 22 12 22C6.477 22 2 17.523 2 12ZM6.34315 6.34315C4.84285 7.84344 4 9.87827 4 12C4 14.1217 4.84285 16.1566 6.34315 17.6569C7.84344 19.1571 9.87827 20 12 20C14.1217 20 16.1566 19.1571 17.6569 17.6569C19.1571 16.1566 20 14.1217 20 12C20 9.87827 19.1571 7.84344 17.6569 6.34315C16.1566 4.84285 14.1217 4 12 4C9.87827 4 7.84344 4.84285 6.34315 6.34315ZM14.4456 8.24707C14.3242 8.29735 14.2139 8.37105 14.121 8.46396L12 10.586L9.87895 8.46396C9.78604 8.37112 9.67575 8.29748 9.55439 8.24726C9.43302 8.19704 9.30295 8.17121 9.1716 8.17126C9.04025 8.1713 8.9102 8.19722 8.78886 8.24753C8.66753 8.29784 8.5573 8.37155 8.46445 8.46446C8.37161 8.55737 8.29797 8.66766 8.24775 8.78903C8.19753 8.91039 8.1717 9.04047 8.17175 9.17181C8.1718 9.30316 8.19771 9.43321 8.24802 9.55455C8.29833 9.67588 8.37204 9.78612 8.46495 9.87896L10.585 12L8.46495 14.121C8.37204 14.2138 8.29833 14.3241 8.24802 14.4454C8.19771 14.5667 8.1718 14.6968 8.17175 14.8281C8.1717 14.9595 8.19753 15.0896 8.24775 15.2109C8.29797 15.3323 8.37161 15.4426 8.46445 15.5355C8.5573 15.6284 8.66753 15.7021 8.78886 15.7524C8.9102 15.8027 9.04025 15.8287 9.1716 15.8287C9.30295 15.8287 9.43302 15.8029 9.55439 15.7527C9.67575 15.7025 9.78604 15.6288 9.87895 15.536L12.001 13.414L14.121 15.536C14.3086 15.7236 14.5631 15.8291 14.8285 15.8291C15.0939 15.8291 15.3484 15.7236 15.536 15.536C15.7236 15.3484 15.8291 15.0939 15.8291 14.8285C15.8291 14.5631 15.7236 14.3086 15.536 14.121L13.414 12L15.536 9.87896C15.6289 9.78605 15.7026 9.67575 15.7529 9.55436C15.8032 9.43296 15.8291 9.30286 15.8291 9.17146C15.8291 9.04007 15.8032 8.90996 15.7529 8.78856C15.7026 8.66717 15.6289 8.55687 15.536 8.46396C15.4431 8.37105 15.3328 8.29735 15.2114 8.24707C15.09 8.19678 14.9599 8.1709 14.8285 8.1709C14.6971 8.1709 14.567 8.19678 14.4456 8.24707Z" fill="#808080"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.0965 6.92857C9.94415 6.32452 10.9591 5.99988 12 5.99988C13.326 5.99988 14.5978 6.52667 15.5355 7.46435C16.4732 8.40203 17 9.6738 17 10.9999C17 11.2651 17.1053 11.5195 17.2929 11.707C17.4804 11.8945 17.7347 11.9999 18 11.9999C18.7956 11.9999 19.5587 12.316 20.1213 12.8786C20.6839 13.4412 21 14.2042 21 14.9999C21 15.7955 20.6839 16.5586 20.1213 17.1212C19.5587 17.6838 18.7956 17.9999 18 17.9999C17.7347 17.9999 17.4804 18.1052 17.2929 18.2928C17.1053 18.4803 17 18.7347 17 18.9999C17 19.2651 17.1053 19.5195 17.2929 19.707C17.4804 19.8945 17.7347 19.9999 18 19.9999C19.2423 19.9964 20.439 19.5308 21.3569 18.6936C22.2748 17.8563 22.8483 16.7075 22.9657 15.4706C23.0832 14.2338 22.7361 12.9976 21.9921 12.0026C21.2481 11.0076 20.1605 10.325 18.941 10.0879C18.7429 8.57347 18.055 7.16539 16.9821 6.07838C15.9092 4.99136 14.5103 4.28506 12.9986 4.0672C11.4869 3.84933 9.94545 4.13186 8.60929 4.87168C7.27313 5.6115 6.21559 6.76803 5.59797 8.16488C4.16853 8.50721 2.91447 9.36235 2.07375 10.568C1.23303 11.7737 0.864124 13.2461 1.03704 14.7057C1.20995 16.1654 1.91265 17.5108 3.01181 18.4867C4.11096 19.4625 5.5301 20.001 6.99997 19.9999C7.26518 19.9999 7.51954 19.8945 7.70707 19.707C7.89461 19.5195 7.99997 19.2651 7.99997 18.9999C7.99997 18.7347 7.89461 18.4803 7.70707 18.2928C7.51954 18.1052 7.26518 17.9999 6.99997 17.9999C5.98734 17.9976 5.01325 17.6114 4.27413 16.9192C3.53501 16.2271 3.08584 15.2804 3.01721 14.2701C2.94858 13.2598 3.26559 12.261 3.90431 11.4752C4.54304 10.6895 5.45595 10.1751 6.45897 10.0359C6.64276 10.0111 6.81603 9.93567 6.95943 9.81805C7.10282 9.70044 7.21069 9.54527 7.27097 9.36988C7.61071 8.38603 8.24885 7.53263 9.0965 6.92857ZM11.2929 12.2929C11.4804 12.1054 11.7348 12 12 12C12.2652 12 12.5196 12.1054 12.7071 12.2929C12.8946 12.4804 13 12.7348 13 13V16.5859L13.293 16.2929C13.4816 16.1108 13.7342 16.01 13.9964 16.0122C14.2586 16.0145 14.5094 16.1197 14.6948 16.3051C14.8803 16.4905 14.9854 16.7413 14.9877 17.0035C14.99 17.2657 14.8892 17.5183 14.707 17.7069L12.7206 19.6933C12.7162 19.698 12.7117 19.7026 12.7071 19.7071C12.5196 19.8946 12.2652 20 12 20C11.7348 20 11.4804 19.8946 11.2929 19.7071C11.2869 19.7011 11.281 19.695 11.2751 19.6889L9.29303 17.7068C9.10556 17.5193 9.00024 17.2649 9.00024 16.9998C9.00024 16.7346 9.10556 16.4803 9.29303 16.2928C9.48056 16.1053 9.73487 16 10 16C10.2652 16 10.5195 16.1053 10.707 16.2928L11 16.5858V13C11 12.7348 11.1054 12.4804 11.2929 12.2929Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 11C14.2091 11 16 9.20914 16 7C16 4.79086 14.2091 3 12 3C9.79086 3 8 4.79086 8 7C8 9.20914 9.79086 11 12 11ZM12 9C13.1046 9 14 8.10457 14 7C14 5.89543 13.1046 5 12 5C10.8954 5 10 5.89543 10 7C10 8.10457 10.8954 9 12 9ZM6.5 4C4.567 4 3 5.567 3 7.5C3 9.433 4.567 11 6.5 11C7.05228 11 7.5 10.5523 7.5 10C7.5 9.44772 7.05228 9 6.5 9C5.67157 9 5 8.32843 5 7.5C5 6.67157 5.67157 6 6.5 6C7.05228 6 7.5 5.55228 7.5 5C7.5 4.44772 7.05228 4 6.5 4ZM5 19C5 15.134 8.13401 12 12 12C15.866 12 19 15.134 19 19C19 19.397 18.9669 19.7869 18.903 20.1671C18.8115 20.7118 18.2958 21.0791 17.7511 20.9876C17.2065 20.8961 16.8391 20.3804 16.9306 19.8357C16.9762 19.5646 17 19.2855 17 19C17 16.2386 14.7614 14 12 14C9.23858 14 7 16.2386 7 19C7 19.2864 7.02397 19.5664 7.06981 19.8383C7.16161 20.3829 6.79454 20.8988 6.24994 20.9906C5.70533 21.0824 5.18943 20.7154 5.09763 20.1708C5.03335 19.7894 5 19.3982 5 19ZM4.85402 13.7725C5.28304 13.4247 5.34889 12.7949 5.0011 12.3659C4.6533 11.9369 4.02357 11.8711 3.59455 12.2188C2.01345 13.5006 1 15.4618 1 17.659C1 18.0572 1.03335 18.4484 1.09763 18.8297C1.18943 19.3743 1.70533 19.7414 2.24994 19.6496C2.79454 19.5578 3.16161 19.0419 3.06981 18.4973C3.02397 18.2253 3 17.9453 3 17.659C3 16.0903 3.72123 14.6908 4.85402 13.7725ZM21.5 7.5C21.5 5.567 19.933 4 18 4C17.4477 4 17 4.44772 17 5C17 5.55228 17.4477 6 18 6C18.8284 6 19.5 6.67157 19.5 7.5C19.5 8.32843 18.8284 9 18 9C17.4477 9 17 9.44772 17 10C17 10.5523 17.4477 11 18 11C19.933 11 21.5 9.433 21.5 7.5ZM19.3703 13.7725C18.9413 13.4247 18.8755 12.7949 19.2233 12.3659C19.5711 11.9369 20.2008 11.8711 20.6298 12.2188C22.2109 13.5006 23.2244 15.4618 23.2244 17.659C23.2244 18.0572 23.191 18.4484 23.1267 18.8297C23.0349 19.3743 22.519 19.7414 21.9744 19.6496C21.4298 19.5578 21.0628 19.0419 21.1546 18.4973C21.2004 18.2253 21.2244 17.9453 21.2244 17.659C21.2244 16.0903 20.5031 14.6908 19.3703 13.7725Z" fill="#006CFF"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 6V3C7 2.73478 7.10536 2.48043 7.29289 2.29289C7.48043 2.10536 7.73478 2 8 2H20C20.2652 2 20.5196 2.10536 20.7071 2.29289C20.8946 2.48043 21 2.73478 21 3V17C21 17.2652 20.8946 17.5196 20.7071 17.7071C20.5196 17.8946 20.2652 18 20 18H17V21C17 21.552 16.55 22 15.993 22H4.007C3.87555 22.0008 3.74522 21.9757 3.62347 21.9261C3.50172 21.8765 3.39093 21.8035 3.29742 21.7111C3.20391 21.6187 3.12952 21.5088 3.07849 21.3876C3.02746 21.2665 3.00079 21.1365 3 21.005V21L3.003 7C3.003 6.448 3.453 6 4.01 6H7ZM5.003 8L5 20H15V8H5.003ZM17 6H9V4H19V16H17V6Z" fill="#808080"/>
</svg>

Before

Width:  |  Height:  |  Size: 718 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.66675 5.33339C2.66675 5.33342 2.66675 5.33346 2.66675 5.33349C2.66675 5.51032 2.73697 5.67991 2.86197 5.80499L4.86197 7.80499C4.92332 7.86941 4.99694 7.92091 5.07849 7.95645C5.16004 7.99199 5.24788 8.01086 5.33684 8.01194C5.42579 8.01302 5.51406 7.9963 5.59646 7.96276C5.67885 7.92922 5.7537 7.87953 5.81661 7.81663C5.87951 7.75373 5.9292 7.67887 5.96274 7.59648C5.99628 7.51408 6.013 7.42581 6.01192 7.33686C6.01083 7.2479 5.99197 7.16007 5.95642 7.07851C5.92088 6.99696 5.86939 6.92334 5.80497 6.86199L4.95612 6.01314C6.62432 6.04367 7.6477 6.12838 8.36399 6.27799C9.21799 6.45499 9.62699 6.72099 10.246 7.18499C10.882 7.74299 11.472 8.34299 11.912 9.16699C12.353 9.99599 12.667 11.094 12.667 12.667C12.6737 12.8393 12.7468 13.0023 12.8711 13.1219C12.9953 13.2414 13.1611 13.3082 13.3335 13.3082C13.5059 13.3082 13.6717 13.2414 13.7959 13.1219C13.9202 13.0023 13.9933 12.8393 14 12.667C14 10.906 13.647 9.58799 13.088 8.54099C12.532 7.49799 11.796 6.76899 11.106 6.16499L11.066 6.13299C10.364 5.60599 9.77199 5.20899 8.63599 4.97199C7.79563 4.79705 6.657 4.70933 4.93046 4.6793L5.80497 3.80479C5.86939 3.74344 5.92088 3.66982 5.95642 3.58827C5.99197 3.50671 6.01083 3.41887 6.01192 3.32992C6.013 3.24097 5.99628 3.1527 5.96274 3.0703C5.9292 2.98791 5.87951 2.91305 5.81661 2.85015C5.7537 2.78724 5.67885 2.73756 5.59646 2.70402C5.51406 2.67047 5.42579 2.65375 5.33684 2.65484C5.24788 2.65592 5.16004 2.67479 5.07849 2.71033C4.99694 2.74587 4.92332 2.79737 4.86197 2.86179L2.86197 4.86178C2.7795 4.9443 2.72087 5.0462 2.69053 5.15677C2.67586 5.21019 2.6678 5.26564 2.66684 5.32191C2.66678 5.3257 2.66675 5.32949 2.66675 5.33329" fill="#808080"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.954 7.459C11.6934 7.19579 9.98507 7.06374 7.39542 7.01852L8.70703 5.70692C8.88919 5.51831 8.98998 5.26572 8.9877 5.00351C8.98543 4.74132 8.88026 4.4905 8.69485 4.30509C8.50945 4.11969 8.25863 4.01453 7.99643 4.01224C7.73423 4.00997 7.48163 4.11077 7.29303 4.29292L4.30392 7.28203L4.29843 7.28743L4.29289 7.29289C4.10536 7.48043 4 7.73478 4 8C4 8.26522 4.10536 8.51957 4.29289 8.70711C4.29894 8.71316 4.30506 8.71912 4.31124 8.725L7.29303 11.7068C7.38528 11.8023 7.49562 11.8785 7.61763 11.9309C7.73963 11.9833 7.87085 12.0109 8.00363 12.0121C8.13641 12.0132 8.26809 11.9879 8.39098 11.9376C8.51388 11.8873 8.62553 11.8131 8.71942 11.7192C8.81332 11.6253 8.88758 11.5137 8.93785 11.3908C8.98813 11.2679 9.01343 11.1362 9.01228 11.0034C9.01113 10.8706 8.98354 10.7394 8.93113 10.6174C8.87872 10.4954 8.80254 10.385 8.70703 10.2928L7.43378 9.01954C9.93674 9.06498 11.4715 9.19141 12.546 9.417C13.828 9.683 14.441 10.082 15.37 10.777C16.324 11.614 17.208 12.515 17.868 13.752C18.53 14.994 19 16.64 19 19C19 19.2652 19.1054 19.5196 19.2929 19.7071C19.4804 19.8946 19.7348 20 20 20C20.2652 20 20.5196 19.8946 20.7071 19.7071C20.8946 19.5196 21 19.2652 21 19C21 16.36 20.47 14.38 19.632 12.811C18.798 11.247 17.694 10.153 16.658 9.247C16.6393 9.23062 16.6199 9.21494 16.6 9.2C15.546 8.409 14.658 7.814 12.954 7.459Z" fill="#808080"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 2C8.44771 2 8 2.44771 8 3V4H5C4.44771 4 4 4.44772 4 5C4 5.55228 4.44771 6 5 6H19C19.5523 6 20 5.55228 20 5C20 4.44772 19.5523 4 19 4H16V3C16 2.44771 15.5523 2 15 2H9ZM7 8C7 7.44772 6.55228 7 6 7C5.44772 7 5 7.44772 5 8V19C5 20.6569 6.34315 22 8 22H16C17.6569 22 19 20.6569 19 19V8C19 7.44772 18.5523 7 18 7C17.4477 7 17 7.44772 17 8V19C17 19.5523 16.5523 20 16 20H8C7.44772 20 7 19.5523 7 19V8ZM10 8C10.5523 8 11 8.44771 11 9V17C11 17.5523 10.5523 18 10 18C9.44771 18 9 17.5523 9 17V9C9 8.44771 9.44771 8 10 8ZM15 9C15 8.44771 14.5523 8 14 8C13.4477 8 13 8.44771 13 9V17C13 17.5523 13.4477 18 14 18C14.5523 18 15 17.5523 15 17V9Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 801 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.30605 8.30597C5.11052 8.50222 5.00073 8.76795 5.00073 9.04498C5.00073 9.322 5.11052 9.58774 5.30605 9.78398L11.2061 15.697C11.4321 15.923 11.7351 16.024 12.0291 16C12.17 16.0032 12.3102 15.9778 12.441 15.9252C12.5718 15.8726 12.6906 15.7939 12.7901 15.694L18.6941 9.78398C18.8898 9.58786 18.9998 9.32208 18.9998 9.04498C18.9998 8.76787 18.8898 8.50209 18.6941 8.30597C18.5972 8.20898 18.4821 8.13203 18.3555 8.07952C18.2289 8.02702 18.0931 8 17.9561 8C17.819 8 17.6832 8.02702 17.5566 8.07952C17.43 8.13203 17.3149 8.20898 17.2181 8.30597L11.9981 13.533L6.78005 8.30597C6.68327 8.20901 6.56831 8.13208 6.44176 8.07959C6.31521 8.0271 6.17956 8.00008 6.04255 8.00008C5.90555 8.00008 5.76989 8.0271 5.64334 8.07959C5.51679 8.13208 5.40184 8.20901 5.30505 8.30597H5.30605Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 900 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 4C11.7348 4 11.4804 4.10536 11.2929 4.29289C11.1054 4.48043 11 4.73478 11 5V14.529L6.782 10.306C6.68513 10.209 6.57009 10.1321 6.44346 10.0795C6.31683 10.027 6.18108 10 6.044 10C5.90692 10 5.77118 10.027 5.64454 10.0795C5.51791 10.1321 5.40287 10.209 5.306 10.306C5.11022 10.5021 5.00027 10.7679 5.00027 11.045C5.00027 11.3221 5.11022 11.5879 5.306 11.784L11.21 17.694C11.427 17.911 11.716 18.013 12 17.999C12.284 18.013 12.573 17.911 12.79 17.694L18.694 11.784C18.8898 11.5879 18.9997 11.3221 18.9997 11.045C18.9997 10.7679 18.8898 10.5021 18.694 10.306C18.5971 10.209 18.4821 10.1321 18.3555 10.0795C18.2288 10.027 18.0931 10 17.956 10C17.8189 10 17.6832 10.027 17.5565 10.0795C17.4299 10.1321 17.3149 10.209 17.218 10.306L13 14.529V5C13 4.73478 12.8946 4.48043 12.7071 4.29289C12.5196 4.10536 12.2652 4 12 4ZM5 21C5 20.7348 5.10536 20.4804 5.29289 20.2929C5.48043 20.1054 5.73478 20 6 20H18C18.2652 20 18.5196 20.1054 18.7071 20.2929C18.8946 20.4804 19 20.7348 19 21C19 21.2652 18.8946 21.5196 18.7071 21.7071C18.5196 21.8946 18.2652 22 18 22H6C5.73478 22 5.48043 21.8946 5.29289 21.7071C5.10536 21.5196 5 21.2652 5 21Z" fill="#808080"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M21 21.0001H3V16.7571L16.435 3.32208C16.8255 2.9317 17.4585 2.9317 17.849 3.32208L20.678 6.15108C21.0684 6.54158 21.0684 7.17458 20.678 7.56508L9.243 19.0001H21V21.0001ZM5 19.0001H6.414L15.728 9.68609L14.314 8.27208L5 17.5861V19.0001ZM18.556 6.85808L17.142 8.27208L15.728 6.85808L17.142 5.44408L18.556 6.85808Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 479 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M20 10C20 15.523 15.523 20 10 20C4.477 20 0 15.523 0 10C0 4.477 4.477 0 10 0C15.523 0 20 4.477 20 10ZM15.6569 15.6569C14.1566 17.1571 12.1217 18 10 18C7.87827 18 5.84344 17.1571 4.34315 15.6569C2.84285 14.1566 2 12.1217 2 10C2 7.87827 2.84285 5.84344 4.34315 4.34315C5.84344 2.84285 7.87827 2 10 2C12.1217 2 14.1566 2.84285 15.6569 4.34315C17.1571 5.84344 18 7.87827 18 10C18 12.1217 17.1571 14.1566 15.6569 15.6569ZM7.07416 14.411C7.12237 14.5267 7.19303 14.6317 7.28204 14.7199L7.28004 14.7209C7.46107 14.9 7.70542 15.0004 7.96004 15.0004C8.21466 15.0004 8.45901 14.9 8.64004 14.7209L12.719 10.6739C12.8083 10.5858 12.8792 10.4809 12.9275 10.3652C12.9759 10.2495 13.0008 10.1253 13.0008 9.9999C13.0008 9.87449 12.9759 9.75034 12.9275 9.63464C12.8792 9.51894 12.8083 9.41399 12.719 9.3259L8.64104 5.2789C8.46018 5.09991 8.216 4.99951 7.96154 4.99951C7.70708 4.99951 7.4629 5.09991 7.28204 5.2789C7.19288 5.36715 7.1221 5.4722 7.07379 5.58797C7.02548 5.70375 7.00061 5.82795 7.00061 5.9534C7.00061 6.07885 7.02548 6.20305 7.07379 6.31883C7.1221 6.4346 7.19288 6.53965 7.28204 6.6279L10.68 9.9999L7.28204 13.3719C7.19303 13.4601 7.12237 13.5651 7.07416 13.6808C7.02594 13.7965 7.00112 13.9206 7.00112 14.0459C7.00112 14.1712 7.02594 14.2953 7.07416 14.411Z" fill="#006CFF"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 22C6.477 22 2 17.523 2 12C2 6.477 6.477 2 12 2C17.523 2 22 6.477 22 12C22 17.523 17.523 22 12 22ZM11 15V17H13V15H11ZM11 13V7H13V13H11Z" fill="#FF3742"/>
</svg>

Before

Width:  |  Height:  |  Size: 309 B

-3
View File
@@ -1,3 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.5" fill-rule="evenodd" clip-rule="evenodd" d="M15.213 8.00022C14.9005 6.31539 14.0081 4.79309 12.6906 3.69745C11.3731 2.60181 9.71355 2.00195 7.99999 2.00195C6.28642 2.00195 4.62692 2.60181 3.3094 3.69745C1.99187 4.79309 1.09946 6.31539 0.786987 8.00022C1.09946 9.68506 1.99187 11.2073 3.3094 12.303C4.62692 13.3986 6.28642 13.9985 7.99999 13.9985C9.71355 13.9985 11.3731 13.3986 12.6906 12.303C14.0081 11.2073 14.9005 9.68506 15.213 8.00022V8.00022ZM13.85 8.00022C13.5474 9.3256 12.804 10.509 11.7412 11.3569C10.6785 12.2047 9.35947 12.6667 7.99999 12.6672C6.64034 12.6669 5.32107 12.205 4.25814 11.3572C3.19521 10.5093 2.45158 9.32577 2.14899 8.00022C2.45323 6.67633 3.1975 5.49476 4.26017 4.6486C5.32284 3.80244 6.64108 3.34171 7.99949 3.34171C9.35789 3.34171 10.6761 3.80244 11.7388 4.6486C12.8015 5.49476 13.5457 6.67633 13.85 8.00022V8.00022ZM7.99999 11.0002C7.53836 11 7.083 10.8933 6.66935 10.6884C6.2557 10.4835 5.89493 10.1859 5.6151 9.81873C5.33528 9.45159 5.14396 9.02482 5.05603 8.57164C4.9681 8.11846 4.98594 7.65111 5.10816 7.20596C5.23038 6.7608 5.45367 6.34986 5.76067 6.00511C6.06767 5.66036 6.45009 5.39111 6.87815 5.21832C7.30622 5.04552 7.76839 4.97385 8.22869 5.00887C8.68898 5.04389 9.13499 5.18466 9.53199 5.42022C9.17569 5.30027 8.7893 5.30428 8.43556 5.4316C8.08183 5.55892 7.78152 5.80208 7.58339 6.12158C7.38526 6.44109 7.30096 6.81819 7.34416 7.19165C7.38736 7.56511 7.55553 7.913 7.82137 8.17884C8.0872 8.44468 8.4351 8.61285 8.80856 8.65605C9.18202 8.69925 9.55912 8.61495 9.87862 8.41682C10.1981 8.21869 10.4413 7.91838 10.5686 7.56464C10.6959 7.21091 10.6999 6.82452 10.58 6.46822C10.8505 6.92374 10.9956 7.44279 11.0005 7.97257C11.0054 8.50236 10.8699 9.02399 10.6078 9.48442C10.3457 9.94485 9.9663 10.3277 9.50824 10.5939C9.05019 10.8601 8.5298 11.0003 7.99999 11.0002V11.0002ZM10.58 6.46822C10.4977 6.2247 10.3603 6.00344 10.1785 5.82168C9.99677 5.63991 9.77551 5.50253 9.53199 5.42022C9.96199 5.67622 10.324 6.03722 10.58 6.46822Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

-3
View File
@@ -1,3 +0,0 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.72998 5C4.72998 4.73478 4.83534 4.48043 5.02287 4.29289C5.21041 4.10536 5.46476 4 5.72998 4H7.72998C7.9952 4 8.24955 4.10536 8.43709 4.29289C8.62462 4.48043 8.72998 4.73478 8.72998 5V7C8.72998 7.26522 8.62462 7.51957 8.43709 7.70711C8.24955 7.89464 7.9952 8 7.72998 8H5.72998C5.46476 8 5.21041 7.89464 5.02287 7.70711C4.83534 7.51957 4.72998 7.26522 4.72998 7V5ZM10.73 5C10.73 4.73478 10.8353 4.48043 11.0229 4.29289C11.2104 4.10536 11.4648 4 11.73 4H13.73C13.9952 4 14.2496 4.10536 14.4371 4.29289C14.6246 4.48043 14.73 4.73478 14.73 5V7C14.73 7.26522 14.6246 7.51957 14.4371 7.70711C14.2496 7.89464 13.9952 8 13.73 8H11.73C11.4648 8 11.2104 7.89464 11.0229 7.70711C10.8353 7.51957 10.73 7.26522 10.73 7V5ZM17.73 4C17.4648 4 17.2104 4.10536 17.0229 4.29289C16.8353 4.48043 16.73 4.73478 16.73 5V7C16.73 7.26522 16.8353 7.51957 17.0229 7.70711C17.2104 7.89464 17.4648 8 17.73 8H19.73C19.9952 8 20.2496 7.89464 20.4371 7.70711C20.6246 7.51957 20.73 7.26522 20.73 7V5C20.73 4.73478 20.6246 4.48043 20.4371 4.29289C20.2496 4.10536 19.9952 4 19.73 4H17.73ZM4.72998 11C4.72998 10.7348 4.83534 10.4804 5.02287 10.2929C5.21041 10.1054 5.46476 10 5.72998 10H7.72998C7.9952 10 8.24955 10.1054 8.43709 10.2929C8.62462 10.4804 8.72998 10.7348 8.72998 11V13C8.72998 13.2652 8.62462 13.5196 8.43709 13.7071C8.24955 13.8946 7.9952 14 7.72998 14H5.72998C5.46476 14 5.21041 13.8946 5.02287 13.7071C4.83534 13.5196 4.72998 13.2652 4.72998 13V11ZM11.73 10C11.4648 10 11.2104 10.1054 11.0229 10.2929C10.8353 10.4804 10.73 10.7348 10.73 11V13C10.73 13.2652 10.8353 13.5196 11.0229 13.7071C11.2104 13.8946 11.4648 14 11.73 14H13.73C13.9952 14 14.2496 13.8946 14.4371 13.7071C14.6246 13.5196 14.73 13.2652 14.73 13V11C14.73 10.7348 14.6246 10.4804 14.4371 10.2929C14.2496 10.1054 13.9952 10 13.73 10H11.73ZM16.73 11C16.73 10.7348 16.8353 10.4804 17.0229 10.2929C17.2104 10.1054 17.4648 10 17.73 10H19.73C19.9952 10 20.2496 10.1054 20.4371 10.2929C20.6246 10.4804 20.73 10.7348 20.73 11V13C20.73 13.2652 20.6246 13.5196 20.4371 13.7071C20.2496 13.8946 19.9952 14 19.73 14H17.73C17.4648 14 17.2104 13.8946 17.0229 13.7071C16.8353 13.5196 16.73 13.2652 16.73 13V11ZM5.72998 16C5.46476 16 5.21041 16.1054 5.02287 16.2929C4.83534 16.4804 4.72998 16.7348 4.72998 17V19C4.72998 19.2652 4.83534 19.5196 5.02287 19.7071C5.21041 19.8946 5.46476 20 5.72998 20H7.72998C7.9952 20 8.24955 19.8946 8.43709 19.7071C8.62462 19.5196 8.72998 19.2652 8.72998 19V17C8.72998 16.7348 8.62462 16.4804 8.43709 16.2929C8.24955 16.1054 7.9952 16 7.72998 16H5.72998ZM10.73 17C10.73 16.7348 10.8353 16.4804 11.0229 16.2929C11.2104 16.1054 11.4648 16 11.73 16H13.73C13.9952 16 14.2496 16.1054 14.4371 16.2929C14.6246 16.4804 14.73 16.7348 14.73 17V19C14.73 19.2652 14.6246 19.5196 14.4371 19.7071C14.2496 19.8946 13.9952 20 13.73 20H11.73C11.4648 20 11.2104 19.8946 11.0229 19.7071C10.8353 19.5196 10.73 19.2652 10.73 19V17ZM17.73 16C17.4648 16 17.2104 16.1054 17.0229 16.2929C16.8353 16.4804 16.73 16.7348 16.73 17V19C16.73 19.2652 16.8353 19.5196 17.0229 19.7071C17.2104 19.8946 17.4648 20 17.73 20H19.73C19.9952 20 20.2496 19.8946 20.4371 19.7071C20.6246 19.5196 20.73 19.2652 20.73 19V17C20.73 16.7348 20.6246 16.4804 20.4371 16.2929C20.2496 16.1054 19.9952 16 19.73 16H17.73Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

Some files were not shown because too many files have changed in this diff Show More