feat: add GalleryHeader/GalleryFooter theme classes (#546)
* chore: Add ImageHeaderTheme and ImageHeaderThemeData to stream_chat_theme.dart This commit adds the following classes to `stream_chat_theme.dart`: 1. `ImageHeaderTheme`, an `InheritedTheme` widget that can wrap other widgets and provide that sub-tree with an `ImageHeaderThemeData` 2. `ImageHeaderThemeData`, a class that allows for the customization of `ImageHeader` widgets * Make ImageHeaderThemeData constructor const * test: initial ImageHeaderThemeData tests Tests the copyWith, ==, and hashcode functions * test: add tests for default ImageHeaderThemeData values Tests the default values of ImageHeaderThemeData that gets built in a StreamChatThemeData against control values. * Update test name and add comments describing control * fix: use ImageHeaderTheme.of(context) instead of StreamChatTheme.of(context) This ensures that ImageHeader will always look for the closest ImageHeaderThemeData in the widget tree. * test: add dark theme default values test * run dartfmt on test file * test: lerping between ImageHeaderThemeData Added tests that lerp from one ImageHeaderThemeData to another (light to dark and dark to light) * test: merging ImageHeaderThemeData Added tests that merge light and dark ImageHeaderThemeData together (light + dark = dark, dark + light = light) * test: add a half-lerp test Added a test that lerps halway between light and dark ImageHeaderThemeData * chore: Add ImageFooterTheme and ImageFooterThemeData to stream_chat_theme.dart This commit adds the following classes to `stream_chat_theme.dart`: 1. `ImageFooterTheme`, an `InheritedTheme` widget that can wrap other widgets and provide that sub-tree with an `ImageFooterThemeData` 2. `ImageFooterThemeData`, a class that allows for the customization of `ImageFooter` widget * chore: apply ImageFooterThemeData to image_footer.dart * fix: make ImageFooterThemeData constructor const * test: add tests for ImageFooterThemeData * add a missing trailing comma * chore: rename ImageHeader & ImageFooter to GalleryHeader and GalleryFooter The respective theme classes and tests have also been renamed * Run dartfmt on test * test: fix failing tests due to renaming
This commit is contained in:
@@ -6,8 +6,8 @@ import 'package:chewie/chewie.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:jiffy/jiffy.dart';
|
import 'package:jiffy/jiffy.dart';
|
||||||
import 'package:photo_view/photo_view.dart';
|
import 'package:photo_view/photo_view.dart';
|
||||||
import 'package:stream_chat_flutter/src/image_footer.dart';
|
import 'package:stream_chat_flutter/src/gallery_footer.dart';
|
||||||
import 'package:stream_chat_flutter/src/image_header.dart';
|
import 'package:stream_chat_flutter/src/gallery_header.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
|
|
||||||
@@ -180,7 +180,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
ImageHeader(
|
GalleryHeader(
|
||||||
userName: widget.userName,
|
userName: widget.userName,
|
||||||
sentAt:
|
sentAt:
|
||||||
// ignore: lines_longer_than_80_chars
|
// ignore: lines_longer_than_80_chars
|
||||||
@@ -198,7 +198,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (widget.message.type != 'ephemeral')
|
if (widget.message.type != 'ephemeral')
|
||||||
ImageFooter(
|
GalleryFooter(
|
||||||
currentPage: _currentPage,
|
currentPage: _currentPage,
|
||||||
totalPages: widget.mediaAttachments.length,
|
totalPages: widget.mediaAttachments.length,
|
||||||
mediaAttachments: widget.mediaAttachments,
|
mediaAttachments: widget.mediaAttachments,
|
||||||
|
|||||||
+16
-13
@@ -13,9 +13,9 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
|||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
/// Footer widget for media display
|
/// Footer widget for media display
|
||||||
class ImageFooter extends StatefulWidget implements PreferredSizeWidget {
|
class GalleryFooter extends StatefulWidget implements PreferredSizeWidget {
|
||||||
/// Creates a channel header
|
/// Creates a channel header
|
||||||
const ImageFooter({
|
const GalleryFooter({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.message,
|
required this.message,
|
||||||
this.onBackPressed,
|
this.onBackPressed,
|
||||||
@@ -54,13 +54,13 @@ class ImageFooter extends StatefulWidget implements PreferredSizeWidget {
|
|||||||
final ValueChanged<int>? mediaSelectedCallBack;
|
final ValueChanged<int>? mediaSelectedCallBack;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_ImageFooterState createState() => _ImageFooterState();
|
_GalleryFooterState createState() => _GalleryFooterState();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final Size preferredSize;
|
final Size preferredSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ImageFooterState extends State<ImageFooter> {
|
class _GalleryFooterState extends State<GalleryFooter> {
|
||||||
final TextEditingController _messageController = TextEditingController();
|
final TextEditingController _messageController = TextEditingController();
|
||||||
final FocusNode _messageFocusNode = FocusNode();
|
final FocusNode _messageFocusNode = FocusNode();
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
const showShareButton = !kIsWeb;
|
const showShareButton = !kIsWeb;
|
||||||
final mediaQueryData = MediaQuery.of(context);
|
final mediaQueryData = MediaQuery.of(context);
|
||||||
final chatThemeData = StreamChatTheme.of(context);
|
final galleryFooterThemeData = GalleryFooterTheme.of(context);
|
||||||
return SizedBox.fromSize(
|
return SizedBox.fromSize(
|
||||||
size: Size(
|
size: Size(
|
||||||
mediaQueryData.size.width,
|
mediaQueryData.size.width,
|
||||||
@@ -88,7 +88,7 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
context: context,
|
context: context,
|
||||||
removeTop: true,
|
removeTop: true,
|
||||||
child: BottomAppBar(
|
child: BottomAppBar(
|
||||||
color: chatThemeData.colorTheme.barsBg,
|
color: galleryFooterThemeData.backgroundColor,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
@@ -100,7 +100,7 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: StreamSvgIcon.iconShare(
|
icon: StreamSvgIcon.iconShare(
|
||||||
size: 24,
|
size: 24,
|
||||||
color: chatThemeData.colorTheme.textHighEmphasis,
|
color: galleryFooterThemeData.shareIconColor,
|
||||||
),
|
),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final attachment =
|
final attachment =
|
||||||
@@ -136,7 +136,7 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
'${widget.currentPage + 1} of ${widget.totalPages}',
|
'${widget.currentPage + 1} of ${widget.totalPages}',
|
||||||
style: chatThemeData.textTheme.headlineBold,
|
style: galleryFooterThemeData.titleTextStyle,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -144,7 +144,7 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: StreamSvgIcon.iconGrid(
|
icon: StreamSvgIcon.iconGrid(
|
||||||
color: chatThemeData.colorTheme.textHighEmphasis,
|
color: galleryFooterThemeData.gridIconButtonColor,
|
||||||
),
|
),
|
||||||
onPressed: () => _showPhotosModal(context),
|
onPressed: () => _showPhotosModal(context),
|
||||||
),
|
),
|
||||||
@@ -157,10 +157,11 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
|
|
||||||
void _showPhotosModal(context) {
|
void _showPhotosModal(context) {
|
||||||
final chatThemeData = StreamChatTheme.of(context);
|
final chatThemeData = StreamChatTheme.of(context);
|
||||||
|
final galleryFooterThemeData = GalleryFooterTheme.of(context);
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
barrierColor: chatThemeData.colorTheme.overlay,
|
barrierColor: galleryFooterThemeData.bottomSheetBarrierColor,
|
||||||
backgroundColor: chatThemeData.colorTheme.barsBg,
|
backgroundColor: galleryFooterThemeData.bottomSheetBackgroundColor,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
@@ -191,7 +192,8 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Photos',
|
'Photos',
|
||||||
style: chatThemeData.textTheme.headlineBold,
|
style:
|
||||||
|
galleryFooterThemeData.bottomSheetPhotosTextStyle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -199,7 +201,8 @@ class _ImageFooterState extends State<ImageFooter> {
|
|||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: StreamSvgIcon.close(
|
icon: StreamSvgIcon.close(
|
||||||
color: chatThemeData.colorTheme.textHighEmphasis,
|
color:
|
||||||
|
galleryFooterThemeData.bottomSheetCloseIconColor,
|
||||||
),
|
),
|
||||||
onPressed: () => Navigator.maybePop(context),
|
onPressed: () => Navigator.maybePop(context),
|
||||||
),
|
),
|
||||||
+11
-9
@@ -5,9 +5,9 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
|||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
/// Header/AppBar widget for media display screen
|
/// Header/AppBar widget for media display screen
|
||||||
class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
|
class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||||
/// Creates a channel header
|
/// Creates a channel header
|
||||||
const ImageHeader({
|
const GalleryHeader({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.message,
|
required this.message,
|
||||||
this.currentIndex = 0,
|
this.currentIndex = 0,
|
||||||
@@ -51,7 +51,7 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final chatThemeData = StreamChatTheme.of(context);
|
final galleryHeaderThemeData = GalleryHeaderTheme.of(context);
|
||||||
return AppBar(
|
return AppBar(
|
||||||
textTheme: Theme.of(context).textTheme,
|
textTheme: Theme.of(context).textTheme,
|
||||||
brightness: Theme.of(context).brightness,
|
brightness: Theme.of(context).brightness,
|
||||||
@@ -59,18 +59,18 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
leading: showBackButton
|
leading: showBackButton
|
||||||
? IconButton(
|
? IconButton(
|
||||||
icon: StreamSvgIcon.close(
|
icon: StreamSvgIcon.close(
|
||||||
color: chatThemeData.colorTheme.textHighEmphasis,
|
color: galleryHeaderThemeData.closeButtonColor,
|
||||||
size: 24,
|
size: 24,
|
||||||
),
|
),
|
||||||
onPressed: onBackPressed,
|
onPressed: onBackPressed,
|
||||||
)
|
)
|
||||||
: const SizedBox(),
|
: const SizedBox(),
|
||||||
backgroundColor: chatThemeData.channelTheme.channelHeaderTheme.color,
|
backgroundColor: galleryHeaderThemeData.backgroundColor,
|
||||||
actions: <Widget>[
|
actions: <Widget>[
|
||||||
if (message.type != 'ephemeral')
|
if (message.type != 'ephemeral')
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: StreamSvgIcon.iconMenuPoint(
|
icon: StreamSvgIcon.iconMenuPoint(
|
||||||
color: chatThemeData.colorTheme.textHighEmphasis,
|
color: galleryHeaderThemeData.iconMenuPointColor,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_showMessageActionModalBottomSheet(context);
|
_showMessageActionModalBottomSheet(context);
|
||||||
@@ -90,11 +90,11 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
userName,
|
userName,
|
||||||
style: chatThemeData.textTheme.headlineBold,
|
style: galleryHeaderThemeData.titleTextStyle,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
sentAt,
|
sentAt,
|
||||||
style: chatThemeData.channelPreviewTheme.subtitle,
|
style: galleryHeaderThemeData.subtitleTextStyle,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -109,11 +109,13 @@ class ImageHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
|
|
||||||
void _showMessageActionModalBottomSheet(BuildContext context) async {
|
void _showMessageActionModalBottomSheet(BuildContext context) async {
|
||||||
final channel = StreamChannel.of(context).channel;
|
final channel = StreamChannel.of(context).channel;
|
||||||
|
final galleryHeaderThemeData =
|
||||||
|
StreamChatTheme.of(context).galleryHeaderTheme;
|
||||||
|
|
||||||
final result = await showDialog(
|
final result = await showDialog(
|
||||||
useRootNavigator: false,
|
useRootNavigator: false,
|
||||||
context: context,
|
context: context,
|
||||||
barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
|
barrierColor: galleryHeaderThemeData.bottomSheetBarrierColor,
|
||||||
builder: (context) => StreamChannel(
|
builder: (context) => StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: AttachmentActionsModal(
|
child: AttachmentActionsModal(
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/src/channel_header.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/channel_preview.dart';
|
||||||
@@ -58,6 +59,8 @@ class StreamChatThemeData {
|
|||||||
Widget Function(BuildContext, User)? defaultUserImage,
|
Widget Function(BuildContext, User)? defaultUserImage,
|
||||||
IconThemeData? primaryIconTheme,
|
IconThemeData? primaryIconTheme,
|
||||||
List<ReactionIcon>? reactionIcons,
|
List<ReactionIcon>? reactionIcons,
|
||||||
|
GalleryHeaderThemeData? imageHeaderTheme,
|
||||||
|
GalleryFooterThemeData? imageFooterTheme,
|
||||||
}) {
|
}) {
|
||||||
brightness ??= colorTheme?.brightness ?? Brightness.light;
|
brightness ??= colorTheme?.brightness ?? Brightness.light;
|
||||||
final isDark = brightness == Brightness.dark;
|
final isDark = brightness == Brightness.dark;
|
||||||
@@ -80,6 +83,8 @@ class StreamChatThemeData {
|
|||||||
defaultUserImage: defaultUserImage,
|
defaultUserImage: defaultUserImage,
|
||||||
primaryIconTheme: primaryIconTheme,
|
primaryIconTheme: primaryIconTheme,
|
||||||
reactionIcons: reactionIcons,
|
reactionIcons: reactionIcons,
|
||||||
|
galleryHeaderTheme: imageHeaderTheme,
|
||||||
|
galleryFooterTheme: imageFooterTheme,
|
||||||
);
|
);
|
||||||
|
|
||||||
return defaultData.merge(customizedData);
|
return defaultData.merge(customizedData);
|
||||||
@@ -107,6 +112,8 @@ class StreamChatThemeData {
|
|||||||
required this.defaultUserImage,
|
required this.defaultUserImage,
|
||||||
required this.primaryIconTheme,
|
required this.primaryIconTheme,
|
||||||
required this.reactionIcons,
|
required this.reactionIcons,
|
||||||
|
required this.galleryHeaderTheme,
|
||||||
|
required this.galleryFooterTheme,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Create a theme from a Material [Theme]
|
/// Create a theme from a Material [Theme]
|
||||||
@@ -136,6 +143,14 @@ class StreamChatThemeData {
|
|||||||
/// Theme of the chat widgets dedicated to a channel
|
/// Theme of the chat widgets dedicated to a channel
|
||||||
final ChannelTheme channelTheme;
|
final ChannelTheme channelTheme;
|
||||||
|
|
||||||
|
/// The default style for [GalleryHeader]s below the overall
|
||||||
|
/// [StreamChatTheme].
|
||||||
|
final GalleryHeaderThemeData galleryHeaderTheme;
|
||||||
|
|
||||||
|
/// The default style for [GalleryFooter]s below the overall
|
||||||
|
/// [StreamChatTheme].
|
||||||
|
final GalleryFooterThemeData galleryFooterTheme;
|
||||||
|
|
||||||
/// Theme of the current user messages
|
/// Theme of the current user messages
|
||||||
final MessageTheme ownMessageTheme;
|
final MessageTheme ownMessageTheme;
|
||||||
|
|
||||||
@@ -172,6 +187,8 @@ class StreamChatThemeData {
|
|||||||
IconThemeData? primaryIconTheme,
|
IconThemeData? primaryIconTheme,
|
||||||
ChannelListHeaderTheme? channelListHeaderTheme,
|
ChannelListHeaderTheme? channelListHeaderTheme,
|
||||||
List<ReactionIcon>? reactionIcons,
|
List<ReactionIcon>? reactionIcons,
|
||||||
|
GalleryHeaderThemeData? galleryHeaderTheme,
|
||||||
|
GalleryFooterThemeData? galleryFooterTheme,
|
||||||
}) =>
|
}) =>
|
||||||
StreamChatThemeData.raw(
|
StreamChatThemeData.raw(
|
||||||
channelListHeaderTheme:
|
channelListHeaderTheme:
|
||||||
@@ -188,6 +205,8 @@ class StreamChatThemeData {
|
|||||||
otherMessageTheme: this.otherMessageTheme.merge(otherMessageTheme),
|
otherMessageTheme: this.otherMessageTheme.merge(otherMessageTheme),
|
||||||
messageInputTheme: this.messageInputTheme.merge(messageInputTheme),
|
messageInputTheme: this.messageInputTheme.merge(messageInputTheme),
|
||||||
reactionIcons: reactionIcons ?? this.reactionIcons,
|
reactionIcons: reactionIcons ?? this.reactionIcons,
|
||||||
|
galleryHeaderTheme: galleryHeaderTheme ?? this.galleryHeaderTheme,
|
||||||
|
galleryFooterTheme: galleryFooterTheme ?? this.galleryFooterTheme,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Merge themes
|
/// Merge themes
|
||||||
@@ -207,6 +226,8 @@ class StreamChatThemeData {
|
|||||||
otherMessageTheme: otherMessageTheme.merge(other.otherMessageTheme),
|
otherMessageTheme: otherMessageTheme.merge(other.otherMessageTheme),
|
||||||
messageInputTheme: messageInputTheme.merge(other.messageInputTheme),
|
messageInputTheme: messageInputTheme.merge(other.messageInputTheme),
|
||||||
reactionIcons: other.reactionIcons,
|
reactionIcons: other.reactionIcons,
|
||||||
|
galleryHeaderTheme: galleryHeaderTheme.merge(other.galleryHeaderTheme),
|
||||||
|
galleryFooterTheme: galleryFooterTheme.merge(other.galleryFooterTheme),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,6 +240,40 @@ class StreamChatThemeData {
|
|||||||
final accentColor = colorTheme.accentPrimary;
|
final accentColor = colorTheme.accentPrimary;
|
||||||
final iconTheme =
|
final iconTheme =
|
||||||
IconThemeData(color: colorTheme.textHighEmphasis.withOpacity(.5));
|
IconThemeData(color: colorTheme.textHighEmphasis.withOpacity(.5));
|
||||||
|
final channelTheme = ChannelTheme(
|
||||||
|
channelHeaderTheme: ChannelHeaderTheme(
|
||||||
|
avatarTheme: AvatarTheme(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
constraints: const BoxConstraints.tightFor(
|
||||||
|
height: 40,
|
||||||
|
width: 40,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
color: colorTheme.barsBg,
|
||||||
|
title: textTheme.headlineBold,
|
||||||
|
subtitle: textTheme.footnote.copyWith(
|
||||||
|
color: const Color(0xff7A7A7A),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final channelPreviewTheme = ChannelPreviewTheme(
|
||||||
|
unreadCounterColor: colorTheme.accentError,
|
||||||
|
avatarTheme: AvatarTheme(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
constraints: const BoxConstraints.tightFor(
|
||||||
|
height: 40,
|
||||||
|
width: 40,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: textTheme.bodyBold,
|
||||||
|
subtitle: textTheme.footnote.copyWith(
|
||||||
|
color: const Color(0xff7A7A7A),
|
||||||
|
),
|
||||||
|
lastMessageAt: textTheme.footnote.copyWith(
|
||||||
|
color: colorTheme.textHighEmphasis.withOpacity(.5),
|
||||||
|
),
|
||||||
|
indicatorIconSize: 16,
|
||||||
|
);
|
||||||
return StreamChatThemeData.raw(
|
return StreamChatThemeData.raw(
|
||||||
textTheme: textTheme,
|
textTheme: textTheme,
|
||||||
colorTheme: colorTheme,
|
colorTheme: colorTheme,
|
||||||
@@ -231,24 +286,7 @@ class StreamChatThemeData {
|
|||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
channelPreviewTheme: ChannelPreviewTheme(
|
channelPreviewTheme: channelPreviewTheme,
|
||||||
unreadCounterColor: colorTheme.accentError,
|
|
||||||
avatarTheme: AvatarTheme(
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
constraints: const BoxConstraints.tightFor(
|
|
||||||
height: 40,
|
|
||||||
width: 40,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: textTheme.bodyBold,
|
|
||||||
subtitle: textTheme.footnote.copyWith(
|
|
||||||
color: const Color(0xff7A7A7A),
|
|
||||||
),
|
|
||||||
lastMessageAt: textTheme.footnote.copyWith(
|
|
||||||
color: colorTheme.textHighEmphasis.withOpacity(.5),
|
|
||||||
),
|
|
||||||
indicatorIconSize: 16,
|
|
||||||
),
|
|
||||||
channelListHeaderTheme: ChannelListHeaderTheme(
|
channelListHeaderTheme: ChannelListHeaderTheme(
|
||||||
avatarTheme: AvatarTheme(
|
avatarTheme: AvatarTheme(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
@@ -260,22 +298,7 @@ class StreamChatThemeData {
|
|||||||
color: colorTheme.barsBg,
|
color: colorTheme.barsBg,
|
||||||
title: textTheme.headlineBold,
|
title: textTheme.headlineBold,
|
||||||
),
|
),
|
||||||
channelTheme: ChannelTheme(
|
channelTheme: channelTheme,
|
||||||
channelHeaderTheme: ChannelHeaderTheme(
|
|
||||||
avatarTheme: AvatarTheme(
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
constraints: const BoxConstraints.tightFor(
|
|
||||||
height: 40,
|
|
||||||
width: 40,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
color: colorTheme.barsBg,
|
|
||||||
title: textTheme.headlineBold,
|
|
||||||
subtitle: textTheme.footnote.copyWith(
|
|
||||||
color: const Color(0xff7A7A7A),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ownMessageTheme: MessageTheme(
|
ownMessageTheme: MessageTheme(
|
||||||
messageAuthor:
|
messageAuthor:
|
||||||
textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis),
|
textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis),
|
||||||
@@ -407,6 +430,24 @@ class StreamChatThemeData {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
galleryHeaderTheme: GalleryHeaderThemeData(
|
||||||
|
closeButtonColor: colorTheme.textHighEmphasis,
|
||||||
|
backgroundColor: channelTheme.channelHeaderTheme.color,
|
||||||
|
iconMenuPointColor: colorTheme.textHighEmphasis,
|
||||||
|
titleTextStyle: textTheme.headlineBold,
|
||||||
|
subtitleTextStyle: channelPreviewTheme.subtitle,
|
||||||
|
bottomSheetBarrierColor: colorTheme.overlay,
|
||||||
|
),
|
||||||
|
galleryFooterTheme: GalleryFooterThemeData(
|
||||||
|
backgroundColor: colorTheme.barsBg,
|
||||||
|
shareIconColor: colorTheme.textHighEmphasis,
|
||||||
|
titleTextStyle: textTheme.headlineBold,
|
||||||
|
gridIconButtonColor: colorTheme.textHighEmphasis,
|
||||||
|
bottomSheetBarrierColor: colorTheme.overlay,
|
||||||
|
bottomSheetBackgroundColor: colorTheme.barsBg,
|
||||||
|
bottomSheetPhotosTextStyle: textTheme.headlineBold,
|
||||||
|
bottomSheetCloseIconColor: colorTheme.textHighEmphasis,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1287,3 +1328,393 @@ class Effect {
|
|||||||
blur: blur ?? this.blur,
|
blur: blur ?? this.blur,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Overrides the default style of [GalleryHeader] descendants.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [GalleryHeaderThemeData], which is used to configure this theme.
|
||||||
|
class GalleryHeaderTheme extends InheritedTheme {
|
||||||
|
/// Creates an [GalleryHeaderTheme].
|
||||||
|
///
|
||||||
|
/// The [data] parameter must not be null.
|
||||||
|
const GalleryHeaderTheme({
|
||||||
|
Key? key,
|
||||||
|
required this.data,
|
||||||
|
required Widget child,
|
||||||
|
}) : super(key: key, child: child);
|
||||||
|
|
||||||
|
/// The configuration of this theme.
|
||||||
|
final GalleryHeaderThemeData data;
|
||||||
|
|
||||||
|
/// The closest instance of this class that encloses the given context.
|
||||||
|
///
|
||||||
|
/// If there is no enclosing [GalleryHeaderTheme] widget, then
|
||||||
|
/// [StreamChatThemeData.galleryHeaderTheme] is used.
|
||||||
|
///
|
||||||
|
/// Typical usage is as follows:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// ImageHeaderTheme theme = ImageHeaderTheme.of(context);
|
||||||
|
/// ```
|
||||||
|
static GalleryHeaderThemeData of(BuildContext context) {
|
||||||
|
final galleryHeaderTheme =
|
||||||
|
context.dependOnInheritedWidgetOfExactType<GalleryHeaderTheme>();
|
||||||
|
return galleryHeaderTheme?.data ??
|
||||||
|
StreamChatTheme.of(context).galleryHeaderTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget wrap(BuildContext context, Widget child) =>
|
||||||
|
GalleryHeaderTheme(data: data, child: child);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(GalleryHeaderTheme oldWidget) =>
|
||||||
|
data != oldWidget.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A style that overrides the default appearance of [GalleryHeader]s when used
|
||||||
|
/// with [GalleryHeaderTheme] or with the overall [StreamChatTheme]'s
|
||||||
|
/// [StreamChatThemeData.galleryHeaderTheme].
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [GalleryHeaderTheme], the theme which is configured with this class.
|
||||||
|
/// * [StreamChatThemeData.galleryHeaderTheme], which can be used to override
|
||||||
|
/// the default style for [GalleryHeader]s below the overall [StreamChatTheme].
|
||||||
|
class GalleryHeaderThemeData with Diagnosticable {
|
||||||
|
/// Creates an [GalleryHeaderThemeData].
|
||||||
|
const GalleryHeaderThemeData({
|
||||||
|
this.closeButtonColor,
|
||||||
|
this.backgroundColor,
|
||||||
|
this.iconMenuPointColor,
|
||||||
|
this.titleTextStyle,
|
||||||
|
this.subtitleTextStyle,
|
||||||
|
this.bottomSheetBarrierColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The color of the "close" button.
|
||||||
|
///
|
||||||
|
/// Defaults to [ColorTheme.textHighEmphasis].
|
||||||
|
final Color? closeButtonColor;
|
||||||
|
|
||||||
|
/// The background color of the [GalleryHeader] widget.
|
||||||
|
///
|
||||||
|
/// Defaults to [ChannelHeaderTheme.color].
|
||||||
|
final Color? backgroundColor;
|
||||||
|
|
||||||
|
/// Defaults to [ColorTheme.textHighEmphasis].
|
||||||
|
final Color? iconMenuPointColor;
|
||||||
|
|
||||||
|
/// The [TextStyle] to use for the [GalleryHeader] title text.
|
||||||
|
///
|
||||||
|
/// Defaults to [TextTheme.headlineBold].
|
||||||
|
final TextStyle? titleTextStyle;
|
||||||
|
|
||||||
|
/// The [TextStyle] to use for the [GalleryHeader] subtitle text.
|
||||||
|
///
|
||||||
|
/// Defaults to [ChannelPreviewTheme.subtitle].
|
||||||
|
final TextStyle? subtitleTextStyle;
|
||||||
|
|
||||||
|
///
|
||||||
|
final Color? bottomSheetBarrierColor;
|
||||||
|
|
||||||
|
/// Copies this [GalleryHeaderThemeData] to another.
|
||||||
|
GalleryHeaderThemeData copyWith({
|
||||||
|
Color? closeButtonColor,
|
||||||
|
Color? backgroundColor,
|
||||||
|
Color? iconMenuPointColor,
|
||||||
|
TextStyle? titleTextStyle,
|
||||||
|
TextStyle? subtitleTextStyle,
|
||||||
|
Color? bottomSheetBarrierColor,
|
||||||
|
}) =>
|
||||||
|
GalleryHeaderThemeData(
|
||||||
|
closeButtonColor: closeButtonColor ?? this.closeButtonColor,
|
||||||
|
backgroundColor: backgroundColor ?? this.backgroundColor,
|
||||||
|
iconMenuPointColor: iconMenuPointColor ?? this.iconMenuPointColor,
|
||||||
|
titleTextStyle: titleTextStyle ?? this.titleTextStyle,
|
||||||
|
subtitleTextStyle: subtitleTextStyle ?? this.subtitleTextStyle,
|
||||||
|
bottomSheetBarrierColor:
|
||||||
|
bottomSheetBarrierColor ?? this.bottomSheetBarrierColor,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Linearly interpolate between two [GalleryHeader] themes.
|
||||||
|
///
|
||||||
|
/// All the properties must be non-null.
|
||||||
|
GalleryHeaderThemeData lerp(
|
||||||
|
GalleryHeaderThemeData a,
|
||||||
|
GalleryHeaderThemeData b,
|
||||||
|
double t,
|
||||||
|
) =>
|
||||||
|
GalleryHeaderThemeData(
|
||||||
|
closeButtonColor: Color.lerp(a.closeButtonColor, b.closeButtonColor, t),
|
||||||
|
backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
|
||||||
|
iconMenuPointColor:
|
||||||
|
Color.lerp(a.iconMenuPointColor, b.iconMenuPointColor, t),
|
||||||
|
titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t),
|
||||||
|
subtitleTextStyle:
|
||||||
|
TextStyle.lerp(a.subtitleTextStyle, b.subtitleTextStyle, t),
|
||||||
|
bottomSheetBarrierColor:
|
||||||
|
Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Merges one [GalleryHeaderThemeData] with the another
|
||||||
|
GalleryHeaderThemeData merge(GalleryHeaderThemeData? other) {
|
||||||
|
if (other == null) return this;
|
||||||
|
return copyWith(
|
||||||
|
closeButtonColor: other.closeButtonColor,
|
||||||
|
backgroundColor: other.backgroundColor,
|
||||||
|
iconMenuPointColor: other.iconMenuPointColor,
|
||||||
|
titleTextStyle: other.titleTextStyle,
|
||||||
|
subtitleTextStyle: other.subtitleTextStyle,
|
||||||
|
bottomSheetBarrierColor: other.bottomSheetBarrierColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is GalleryHeaderThemeData &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
closeButtonColor == other.closeButtonColor &&
|
||||||
|
backgroundColor == other.backgroundColor &&
|
||||||
|
iconMenuPointColor == other.iconMenuPointColor &&
|
||||||
|
titleTextStyle == other.titleTextStyle &&
|
||||||
|
subtitleTextStyle == other.subtitleTextStyle &&
|
||||||
|
bottomSheetBarrierColor == other.bottomSheetBarrierColor;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
closeButtonColor.hashCode ^
|
||||||
|
backgroundColor.hashCode ^
|
||||||
|
iconMenuPointColor.hashCode ^
|
||||||
|
titleTextStyle.hashCode ^
|
||||||
|
subtitleTextStyle.hashCode ^
|
||||||
|
bottomSheetBarrierColor.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties
|
||||||
|
..add(ColorProperty('closeButtonColor', closeButtonColor))
|
||||||
|
..add(ColorProperty('backgroundColor', backgroundColor))
|
||||||
|
..add(ColorProperty('iconMenuPointColor', iconMenuPointColor))
|
||||||
|
..add(DiagnosticsProperty('titleTextStyle', titleTextStyle))
|
||||||
|
..add(DiagnosticsProperty('subtitleTextStyle', subtitleTextStyle))
|
||||||
|
..add(ColorProperty('bottomSheetBarrierColor', bottomSheetBarrierColor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Overrides the default style of [GalleryFooter] descendants.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [GalleryFooterThemeData], which is used to configure this theme.
|
||||||
|
class GalleryFooterTheme extends InheritedTheme {
|
||||||
|
/// Creates an [GalleryFooterTheme].
|
||||||
|
///
|
||||||
|
/// The [data] parameter must not be null.
|
||||||
|
const GalleryFooterTheme({
|
||||||
|
Key? key,
|
||||||
|
required this.data,
|
||||||
|
required Widget child,
|
||||||
|
}) : super(key: key, child: child);
|
||||||
|
|
||||||
|
/// The configuration of this theme.
|
||||||
|
final GalleryFooterThemeData data;
|
||||||
|
|
||||||
|
/// The closest instance of this class that encloses the given context.
|
||||||
|
///
|
||||||
|
/// If there is no enclosing [GalleryFooterTheme] widget, then
|
||||||
|
/// [StreamChatThemeData.galleryFooterTheme] is used.
|
||||||
|
///
|
||||||
|
/// Typical usage is as follows:
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// ImageFooterTheme theme = ImageFooterTheme.of(context);
|
||||||
|
/// ```
|
||||||
|
static GalleryFooterThemeData of(BuildContext context) {
|
||||||
|
final imageFooterTheme =
|
||||||
|
context.dependOnInheritedWidgetOfExactType<GalleryFooterTheme>();
|
||||||
|
return imageFooterTheme?.data ??
|
||||||
|
StreamChatTheme.of(context).galleryFooterTheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget wrap(BuildContext context, Widget child) =>
|
||||||
|
GalleryFooterTheme(data: data, child: child);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(GalleryFooterTheme oldWidget) =>
|
||||||
|
data != oldWidget.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A style that overrides the default appearance of [GalleryFooter]s when used
|
||||||
|
/// with [GalleryFooterTheme] or with the overall [StreamChatTheme]'s
|
||||||
|
/// [StreamChatThemeData.galleryFooterTheme].
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [GalleryFooterTheme], the theme which is configured with this class.
|
||||||
|
/// * [StreamChatThemeData.galleryFooterTheme], which can be used to override
|
||||||
|
/// the default style for [GalleryFooter]s below the overall [StreamChatTheme].
|
||||||
|
class GalleryFooterThemeData with Diagnosticable {
|
||||||
|
/// Creates an [GalleryFooterThemeData].
|
||||||
|
const GalleryFooterThemeData({
|
||||||
|
this.backgroundColor,
|
||||||
|
this.shareIconColor,
|
||||||
|
this.titleTextStyle,
|
||||||
|
this.gridIconButtonColor,
|
||||||
|
this.bottomSheetBarrierColor,
|
||||||
|
this.bottomSheetBackgroundColor,
|
||||||
|
this.bottomSheetPhotosTextStyle,
|
||||||
|
this.bottomSheetCloseIconColor,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The background color for the [GalleryFooter] widget.
|
||||||
|
///
|
||||||
|
/// Defaults to [ColorTheme.barsBg].
|
||||||
|
final Color? backgroundColor;
|
||||||
|
|
||||||
|
/// The color for the "share" icon.
|
||||||
|
///
|
||||||
|
/// Defaults to [ColorTheme.textHighEmphasis].
|
||||||
|
final Color? shareIconColor;
|
||||||
|
|
||||||
|
/// The [TextStyle] to use for the [GalleryFooter] title text.
|
||||||
|
///
|
||||||
|
/// Defaults to [TextTheme.headlineBold].
|
||||||
|
final TextStyle? titleTextStyle;
|
||||||
|
|
||||||
|
/// The color to use for the "grid" icon.
|
||||||
|
///
|
||||||
|
/// Defaults to [ColorTheme.textHighEmphasis].
|
||||||
|
final Color? gridIconButtonColor;
|
||||||
|
|
||||||
|
/// The color to use behind the bottom sheet.
|
||||||
|
///
|
||||||
|
/// Defaults to [ColorTheme.overlay].
|
||||||
|
final Color? bottomSheetBarrierColor;
|
||||||
|
|
||||||
|
/// The background color to use for the bottom sheet.
|
||||||
|
///
|
||||||
|
/// Defaults to [ColorTheme.barsBg].
|
||||||
|
final Color? bottomSheetBackgroundColor;
|
||||||
|
|
||||||
|
/// The [TextStyle] to use for the "photos" text in the bottom sheet.
|
||||||
|
///
|
||||||
|
/// Defaults to [TextTheme.headlineBold].
|
||||||
|
final TextStyle? bottomSheetPhotosTextStyle;
|
||||||
|
|
||||||
|
/// The color to use for the "close" icon.
|
||||||
|
///
|
||||||
|
/// Defaults to [ColorTheme.textHighEmphasis].
|
||||||
|
final Color? bottomSheetCloseIconColor;
|
||||||
|
|
||||||
|
/// Copies this [GalleryFooterThemeData] to another.
|
||||||
|
GalleryFooterThemeData copyWith({
|
||||||
|
Color? backgroundColor,
|
||||||
|
Color? shareIconColor,
|
||||||
|
TextStyle? titleTextStyle,
|
||||||
|
Color? gridIconButtonColor,
|
||||||
|
Color? bottomSheetBarrierColor,
|
||||||
|
Color? bottomSheetBackgroundColor,
|
||||||
|
TextStyle? bottomSheetPhotosTextStyle,
|
||||||
|
Color? bottomSheetCloseIconColor,
|
||||||
|
}) =>
|
||||||
|
GalleryFooterThemeData(
|
||||||
|
backgroundColor: backgroundColor ?? this.backgroundColor,
|
||||||
|
shareIconColor: shareIconColor ?? this.shareIconColor,
|
||||||
|
titleTextStyle: titleTextStyle ?? this.titleTextStyle,
|
||||||
|
gridIconButtonColor: gridIconButtonColor ?? this.gridIconButtonColor,
|
||||||
|
bottomSheetBarrierColor:
|
||||||
|
bottomSheetBarrierColor ?? this.bottomSheetBarrierColor,
|
||||||
|
bottomSheetBackgroundColor:
|
||||||
|
bottomSheetBackgroundColor ?? this.bottomSheetBackgroundColor,
|
||||||
|
bottomSheetPhotosTextStyle:
|
||||||
|
bottomSheetPhotosTextStyle ?? this.bottomSheetPhotosTextStyle,
|
||||||
|
bottomSheetCloseIconColor:
|
||||||
|
bottomSheetCloseIconColor ?? this.bottomSheetCloseIconColor,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Linearly interpolate between two [GalleryFooter] themes.
|
||||||
|
///
|
||||||
|
/// All the properties must be non-null.
|
||||||
|
GalleryFooterThemeData lerp(
|
||||||
|
GalleryFooterThemeData a,
|
||||||
|
GalleryFooterThemeData b,
|
||||||
|
double t,
|
||||||
|
) =>
|
||||||
|
GalleryFooterThemeData(
|
||||||
|
backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
|
||||||
|
shareIconColor: Color.lerp(a.shareIconColor, b.shareIconColor, t),
|
||||||
|
titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t),
|
||||||
|
gridIconButtonColor:
|
||||||
|
Color.lerp(a.gridIconButtonColor, b.gridIconButtonColor, t),
|
||||||
|
bottomSheetBarrierColor:
|
||||||
|
Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t),
|
||||||
|
bottomSheetBackgroundColor: Color.lerp(
|
||||||
|
a.bottomSheetBackgroundColor, b.bottomSheetBackgroundColor, t),
|
||||||
|
bottomSheetPhotosTextStyle: TextStyle.lerp(
|
||||||
|
a.bottomSheetPhotosTextStyle, b.bottomSheetPhotosTextStyle, t),
|
||||||
|
bottomSheetCloseIconColor: Color.lerp(
|
||||||
|
a.bottomSheetCloseIconColor, b.bottomSheetCloseIconColor, t),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Merges one [GalleryFooterThemeData] with the another
|
||||||
|
GalleryFooterThemeData merge(GalleryFooterThemeData? other) {
|
||||||
|
if (other == null) return this;
|
||||||
|
return copyWith(
|
||||||
|
backgroundColor: other.backgroundColor,
|
||||||
|
bottomSheetBarrierColor: other.bottomSheetBarrierColor,
|
||||||
|
bottomSheetBackgroundColor: other.bottomSheetBackgroundColor,
|
||||||
|
bottomSheetCloseIconColor: other.bottomSheetCloseIconColor,
|
||||||
|
bottomSheetPhotosTextStyle: other.bottomSheetPhotosTextStyle,
|
||||||
|
gridIconButtonColor: other.gridIconButtonColor,
|
||||||
|
titleTextStyle: other.titleTextStyle,
|
||||||
|
shareIconColor: other.shareIconColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is GalleryFooterThemeData &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
backgroundColor == other.backgroundColor &&
|
||||||
|
shareIconColor == other.shareIconColor &&
|
||||||
|
titleTextStyle == other.titleTextStyle &&
|
||||||
|
gridIconButtonColor == other.gridIconButtonColor &&
|
||||||
|
bottomSheetBarrierColor == other.bottomSheetBarrierColor &&
|
||||||
|
bottomSheetBackgroundColor == other.bottomSheetBackgroundColor &&
|
||||||
|
bottomSheetPhotosTextStyle == other.bottomSheetPhotosTextStyle &&
|
||||||
|
bottomSheetCloseIconColor == other.bottomSheetCloseIconColor;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
backgroundColor.hashCode ^
|
||||||
|
shareIconColor.hashCode ^
|
||||||
|
titleTextStyle.hashCode ^
|
||||||
|
gridIconButtonColor.hashCode ^
|
||||||
|
bottomSheetBarrierColor.hashCode ^
|
||||||
|
bottomSheetBackgroundColor.hashCode ^
|
||||||
|
bottomSheetPhotosTextStyle.hashCode ^
|
||||||
|
bottomSheetCloseIconColor.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
|
super.debugFillProperties(properties);
|
||||||
|
properties
|
||||||
|
..add(ColorProperty('backgroundColor', backgroundColor))
|
||||||
|
..add(ColorProperty('shareIconColor', shareIconColor))
|
||||||
|
..add(DiagnosticsProperty('titleTextStyle', titleTextStyle))
|
||||||
|
..add(ColorProperty('gridIconButtonColor', gridIconButtonColor))
|
||||||
|
..add(ColorProperty('bottomSheetBarrierColor', bottomSheetBarrierColor))
|
||||||
|
..add(ColorProperty(
|
||||||
|
'bottomSheetBackgroundColor', bottomSheetBackgroundColor))
|
||||||
|
..add(DiagnosticsProperty(
|
||||||
|
'bottomSheetPhotosTextStyle', bottomSheetPhotosTextStyle))
|
||||||
|
..add(ColorProperty(
|
||||||
|
'bottomSheetCloseIconColor', bottomSheetCloseIconColor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ export 'src/connection_status_builder.dart';
|
|||||||
export 'src/date_divider.dart';
|
export 'src/date_divider.dart';
|
||||||
export 'src/deleted_message.dart';
|
export 'src/deleted_message.dart';
|
||||||
export 'src/full_screen_media.dart';
|
export 'src/full_screen_media.dart';
|
||||||
export 'src/image_footer.dart';
|
export 'src/gallery_footer.dart';
|
||||||
export 'src/image_header.dart';
|
export 'src/gallery_header.dart';
|
||||||
export 'src/info_tile.dart';
|
export 'src/info_tile.dart';
|
||||||
export 'src/mention_tile.dart';
|
export 'src/mention_tile.dart';
|
||||||
export 'src/message_action.dart';
|
export 'src/message_action.dart';
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import 'package:flutter/material.dart' hide TextTheme;
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
class MockStreamChatClient extends Mock implements StreamChatClient {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('GalleryFooterThemeData copyWith, ==, hashCode basics', () {
|
||||||
|
expect(const GalleryFooterThemeData(),
|
||||||
|
const GalleryFooterThemeData().copyWith());
|
||||||
|
expect(const GalleryFooterThemeData().hashCode,
|
||||||
|
const GalleryFooterThemeData().copyWith().hashCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'Light GalleryFooterThemeData lerps completely to dark GalleryFooterThemeData',
|
||||||
|
() {
|
||||||
|
expect(
|
||||||
|
const GalleryFooterThemeData().lerp(_galleryFooterThemeDataControl,
|
||||||
|
_galleryFooterThemeDataControlDark, 1),
|
||||||
|
_galleryFooterThemeDataControlDark);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'Light GalleryFooterThemeData lerps halfway to dark GalleryFooterThemeData',
|
||||||
|
() {
|
||||||
|
expect(
|
||||||
|
const GalleryFooterThemeData().lerp(_galleryFooterThemeDataControl,
|
||||||
|
_galleryFooterThemeDataControlDark, 0.5),
|
||||||
|
_galleryFooterThemeDataControlMidLerp);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'Dark GalleryFooterThemeData lerps completely to light GalleryFooterThemeData',
|
||||||
|
() {
|
||||||
|
expect(
|
||||||
|
const GalleryFooterThemeData().lerp(_galleryFooterThemeDataControlDark,
|
||||||
|
_galleryFooterThemeDataControl, 1),
|
||||||
|
_galleryFooterThemeDataControl);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Merging dark and light themes results in a dark theme', () {
|
||||||
|
expect(
|
||||||
|
_galleryFooterThemeDataControl
|
||||||
|
.merge(_galleryFooterThemeDataControlDark),
|
||||||
|
_galleryFooterThemeDataControlDark);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Merging dark and light themes results in a dark theme', () {
|
||||||
|
expect(
|
||||||
|
_galleryFooterThemeDataControlDark
|
||||||
|
.merge(_galleryFooterThemeDataControl),
|
||||||
|
_galleryFooterThemeDataControl);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Passing no GalleryFooterThemeData returns default light theme values',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
late BuildContext _context;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
builder: (context, child) => StreamChat(
|
||||||
|
client: MockStreamChatClient(),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
home: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
_context = context;
|
||||||
|
return Scaffold(
|
||||||
|
appBar: GalleryFooter(
|
||||||
|
message: Message(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final imageFooterTheme = GalleryFooterTheme.of(_context);
|
||||||
|
expect(imageFooterTheme.backgroundColor,
|
||||||
|
_galleryFooterThemeDataControl.backgroundColor);
|
||||||
|
expect(imageFooterTheme.shareIconColor,
|
||||||
|
_galleryFooterThemeDataControl.shareIconColor);
|
||||||
|
expect(imageFooterTheme.titleTextStyle,
|
||||||
|
_galleryFooterThemeDataControl.titleTextStyle);
|
||||||
|
expect(imageFooterTheme.gridIconButtonColor,
|
||||||
|
_galleryFooterThemeDataControl.gridIconButtonColor);
|
||||||
|
expect(imageFooterTheme.bottomSheetBarrierColor,
|
||||||
|
_galleryFooterThemeDataControl.bottomSheetBarrierColor);
|
||||||
|
expect(imageFooterTheme.bottomSheetBackgroundColor,
|
||||||
|
_galleryFooterThemeDataControl.bottomSheetBackgroundColor);
|
||||||
|
expect(imageFooterTheme.bottomSheetCloseIconColor,
|
||||||
|
_galleryFooterThemeDataControl.bottomSheetCloseIconColor);
|
||||||
|
expect(imageFooterTheme.bottomSheetPhotosTextStyle,
|
||||||
|
_galleryFooterThemeDataControl.bottomSheetPhotosTextStyle);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Passing no GalleryFooterThemeData returns default dark theme values',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
late BuildContext _context;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
builder: (context, child) => StreamChat(
|
||||||
|
client: MockStreamChatClient(),
|
||||||
|
streamChatThemeData: StreamChatThemeData.dark(),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
home: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
_context = context;
|
||||||
|
return Scaffold(
|
||||||
|
appBar: GalleryFooter(
|
||||||
|
message: Message(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final imageFooterTheme = GalleryFooterTheme.of(_context);
|
||||||
|
expect(imageFooterTheme.backgroundColor,
|
||||||
|
_galleryFooterThemeDataControlDark.backgroundColor);
|
||||||
|
expect(imageFooterTheme.shareIconColor,
|
||||||
|
_galleryFooterThemeDataControlDark.shareIconColor);
|
||||||
|
expect(imageFooterTheme.titleTextStyle,
|
||||||
|
_galleryFooterThemeDataControlDark.titleTextStyle);
|
||||||
|
expect(imageFooterTheme.gridIconButtonColor,
|
||||||
|
_galleryFooterThemeDataControlDark.gridIconButtonColor);
|
||||||
|
expect(imageFooterTheme.bottomSheetBarrierColor,
|
||||||
|
_galleryFooterThemeDataControlDark.bottomSheetBarrierColor);
|
||||||
|
expect(imageFooterTheme.bottomSheetBackgroundColor,
|
||||||
|
_galleryFooterThemeDataControlDark.bottomSheetBackgroundColor);
|
||||||
|
expect(imageFooterTheme.bottomSheetCloseIconColor,
|
||||||
|
_galleryFooterThemeDataControlDark.bottomSheetCloseIconColor);
|
||||||
|
expect(imageFooterTheme.bottomSheetPhotosTextStyle,
|
||||||
|
_galleryFooterThemeDataControlDark.bottomSheetPhotosTextStyle);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Light theme control
|
||||||
|
final _galleryFooterThemeDataControl = GalleryFooterThemeData(
|
||||||
|
backgroundColor: ColorTheme.light().barsBg,
|
||||||
|
shareIconColor: ColorTheme.light().textHighEmphasis,
|
||||||
|
titleTextStyle: TextTheme.light().headlineBold,
|
||||||
|
gridIconButtonColor: ColorTheme.light().textHighEmphasis,
|
||||||
|
bottomSheetBackgroundColor: ColorTheme.light().barsBg,
|
||||||
|
bottomSheetBarrierColor: ColorTheme.light().overlay,
|
||||||
|
bottomSheetCloseIconColor: ColorTheme.light().textHighEmphasis,
|
||||||
|
bottomSheetPhotosTextStyle: TextTheme.light().headlineBold,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mid-lerp theme control
|
||||||
|
const _galleryFooterThemeDataControlMidLerp = GalleryFooterThemeData(
|
||||||
|
backgroundColor: Color(0xff87898b),
|
||||||
|
shareIconColor: Color(0xff7f7f7f),
|
||||||
|
titleTextStyle: TextStyle(
|
||||||
|
color: Color(0xff7f7f7f),
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
gridIconButtonColor: Color(0xff7f7f7f),
|
||||||
|
bottomSheetBarrierColor: Color(0x4c000000),
|
||||||
|
bottomSheetBackgroundColor: Color(0xff87898b),
|
||||||
|
bottomSheetPhotosTextStyle: TextStyle(
|
||||||
|
color: Color(0xff7f7f7f),
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
bottomSheetCloseIconColor: Color(0xff7f7f7f),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Dark theme control
|
||||||
|
final _galleryFooterThemeDataControlDark = GalleryFooterThemeData(
|
||||||
|
backgroundColor: ColorTheme.dark().barsBg,
|
||||||
|
shareIconColor: ColorTheme.dark().textHighEmphasis,
|
||||||
|
titleTextStyle: TextTheme.dark().headlineBold,
|
||||||
|
gridIconButtonColor: ColorTheme.dark().textHighEmphasis,
|
||||||
|
bottomSheetBackgroundColor: ColorTheme.dark().barsBg,
|
||||||
|
bottomSheetBarrierColor: ColorTheme.dark().overlay,
|
||||||
|
bottomSheetCloseIconColor: ColorTheme.dark().textHighEmphasis,
|
||||||
|
bottomSheetPhotosTextStyle: TextTheme.dark().headlineBold,
|
||||||
|
);
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
|
class MockStreamChatClient extends Mock implements StreamChatClient {}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('GalleryHeaderThemeData copyWith, ==, hashCode basics', () {
|
||||||
|
expect(const GalleryHeaderThemeData(),
|
||||||
|
const GalleryHeaderThemeData().copyWith());
|
||||||
|
expect(const GalleryHeaderThemeData().hashCode,
|
||||||
|
const GalleryHeaderThemeData().copyWith().hashCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'Light GalleryHeaderThemeData lerps completely to dark GalleryHeaderThemeData',
|
||||||
|
() {
|
||||||
|
expect(
|
||||||
|
const GalleryHeaderThemeData().lerp(_galleryHeaderThemeDataControl,
|
||||||
|
_galleryHeaderThemeDataDarkControl, 1),
|
||||||
|
_galleryHeaderThemeDataDarkControl);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'Light GalleryHeaderThemeData lerps halfway to dark GalleryHeaderThemeData',
|
||||||
|
() {
|
||||||
|
expect(
|
||||||
|
const GalleryHeaderThemeData().lerp(_galleryHeaderThemeDataControl,
|
||||||
|
_galleryHeaderThemeDataDarkControl, 0.5),
|
||||||
|
_galleryHeaderThemeDataHalfLerpControl);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'Dark GalleryHeaderThemeData lerps completely to light GalleryHeaderThemeData',
|
||||||
|
() {
|
||||||
|
expect(
|
||||||
|
const GalleryHeaderThemeData().lerp(_galleryHeaderThemeDataDarkControl,
|
||||||
|
_galleryHeaderThemeDataControl, 1),
|
||||||
|
_galleryHeaderThemeDataControl);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Merging dark and light themes results in a dark theme', () {
|
||||||
|
expect(
|
||||||
|
_galleryHeaderThemeDataControl
|
||||||
|
.merge(_galleryHeaderThemeDataDarkControl),
|
||||||
|
_galleryHeaderThemeDataDarkControl);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Merging dark and light themes results in a dark theme', () {
|
||||||
|
expect(
|
||||||
|
_galleryHeaderThemeDataDarkControl
|
||||||
|
.merge(_galleryHeaderThemeDataControl),
|
||||||
|
_galleryHeaderThemeDataControl);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Passing no GalleryHeaderThemeData returns default light theme values',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
late BuildContext _context;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
builder: (context, child) => StreamChat(
|
||||||
|
client: MockStreamChatClient(),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
home: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
_context = context;
|
||||||
|
return Scaffold(
|
||||||
|
appBar: GalleryHeader(
|
||||||
|
message: Message(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final imageHeaderTheme = GalleryHeaderTheme.of(_context);
|
||||||
|
expect(imageHeaderTheme.closeButtonColor,
|
||||||
|
_galleryHeaderThemeDataControl.closeButtonColor);
|
||||||
|
expect(imageHeaderTheme.backgroundColor,
|
||||||
|
_galleryHeaderThemeDataControl.backgroundColor);
|
||||||
|
expect(imageHeaderTheme.iconMenuPointColor,
|
||||||
|
_galleryHeaderThemeDataControl.iconMenuPointColor);
|
||||||
|
expect(imageHeaderTheme.titleTextStyle,
|
||||||
|
_galleryHeaderThemeDataControl.titleTextStyle);
|
||||||
|
expect(imageHeaderTheme.subtitleTextStyle,
|
||||||
|
_galleryHeaderThemeDataControl.subtitleTextStyle);
|
||||||
|
expect(imageHeaderTheme.bottomSheetBarrierColor,
|
||||||
|
_galleryHeaderThemeDataControl.bottomSheetBarrierColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'Passing no GalleryHeaderThemeData returns default dark theme values',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
late BuildContext _context;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
builder: (context, child) => StreamChat(
|
||||||
|
client: MockStreamChatClient(),
|
||||||
|
streamChatThemeData: StreamChatThemeData.dark(),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
home: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
_context = context;
|
||||||
|
return Scaffold(
|
||||||
|
appBar: GalleryHeader(
|
||||||
|
message: Message(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final imageHeaderTheme = GalleryHeaderTheme.of(_context);
|
||||||
|
expect(imageHeaderTheme.closeButtonColor,
|
||||||
|
_galleryHeaderThemeDataDarkControl.closeButtonColor);
|
||||||
|
expect(imageHeaderTheme.backgroundColor,
|
||||||
|
_galleryHeaderThemeDataDarkControl.backgroundColor);
|
||||||
|
expect(imageHeaderTheme.iconMenuPointColor,
|
||||||
|
_galleryHeaderThemeDataDarkControl.iconMenuPointColor);
|
||||||
|
expect(imageHeaderTheme.titleTextStyle,
|
||||||
|
_galleryHeaderThemeDataDarkControl.titleTextStyle);
|
||||||
|
expect(imageHeaderTheme.subtitleTextStyle,
|
||||||
|
_galleryHeaderThemeDataDarkControl.subtitleTextStyle);
|
||||||
|
expect(imageHeaderTheme.bottomSheetBarrierColor,
|
||||||
|
_galleryHeaderThemeDataDarkControl.bottomSheetBarrierColor);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Light theme test control.
|
||||||
|
final _galleryHeaderThemeDataControl = GalleryHeaderThemeData(
|
||||||
|
closeButtonColor: const Color(0xff000000),
|
||||||
|
backgroundColor: const Color(0xffffffff),
|
||||||
|
iconMenuPointColor: const Color(0xff000000),
|
||||||
|
titleTextStyle: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.black,
|
||||||
|
),
|
||||||
|
subtitleTextStyle: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.black,
|
||||||
|
).copyWith(
|
||||||
|
color: const Color(0xff7A7A7A),
|
||||||
|
),
|
||||||
|
bottomSheetBarrierColor: const Color.fromRGBO(0, 0, 0, 0.2),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Light theme test control.
|
||||||
|
final _galleryHeaderThemeDataHalfLerpControl = GalleryHeaderThemeData(
|
||||||
|
closeButtonColor: const Color(0xff7f7f7f),
|
||||||
|
backgroundColor: const Color(0xff87898b),
|
||||||
|
iconMenuPointColor: const Color(0xff7f7f7f),
|
||||||
|
titleTextStyle: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xff7f7f7f),
|
||||||
|
),
|
||||||
|
subtitleTextStyle: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Color(0xff7a7a7a),
|
||||||
|
).copyWith(
|
||||||
|
color: const Color(0xff7A7A7A),
|
||||||
|
),
|
||||||
|
bottomSheetBarrierColor: const Color(0x4c000000),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Dark theme test control.
|
||||||
|
final _galleryHeaderThemeDataDarkControl = GalleryHeaderThemeData(
|
||||||
|
closeButtonColor: const Color(0xffffffff),
|
||||||
|
backgroundColor: const Color(0xff101418),
|
||||||
|
iconMenuPointColor: const Color(0xffffffff),
|
||||||
|
titleTextStyle: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
subtitleTextStyle: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.white,
|
||||||
|
).copyWith(
|
||||||
|
color: const Color(0xff7A7A7A),
|
||||||
|
),
|
||||||
|
bottomSheetBarrierColor: const Color.fromRGBO(0, 0, 0, 0.4),
|
||||||
|
);
|
||||||
@@ -37,7 +37,7 @@ void main() {
|
|||||||
child: WillPopScope(
|
child: WillPopScope(
|
||||||
onWillPop: () async => false,
|
onWillPop: () async => false,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: ImageFooter(
|
body: GalleryFooter(
|
||||||
message: Message(),
|
message: Message(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user