From bcabbf12db5bb532116cd4d231d5284a8a851c16 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Mon, 26 Jul 2021 14:46:14 -0400 Subject: [PATCH 001/121] chore: refactor most theme related classes into a /theme folder stream_chat_theme.dart remains in /src because I feel it will cause too much of a mess if moved. --- .../lib/src/attachment/attachment_title.dart | 1 + .../lib/src/attachment/image_attachment.dart | 1 + .../lib/src/attachment/video_attachment.dart | 1 + .../lib/src/channel_list_view.dart | 1 + .../lib/src/deleted_message.dart | 1 + .../lib/src/gallery_footer.dart | 1 + .../lib/src/gallery_header.dart | 1 + .../lib/src/image_group.dart | 1 + .../lib/src/message_actions_modal.dart | 1 + .../lib/src/message_list_view.dart | 1 + .../lib/src/message_reactions_modal.dart | 1 + .../lib/src/message_search_list_view.dart | 1 + .../lib/src/message_text.dart | 1 + .../lib/src/message_widget.dart | 1 + .../lib/src/quoted_message_widget.dart | 1 + .../lib/src/stream_chat_theme.dart | 1704 +---------------- .../lib/src/theme/avatar_theme.dart | 44 + .../lib/src/theme/channel_header_theme.dart | 50 + .../src/theme/channel_list_header_theme.dart | 43 + .../src/theme/channel_list_view_theme.dart | 111 ++ .../lib/src/theme/channel_preview_theme.dart | 64 + .../lib/src/theme/channel_theme.dart | 28 + .../lib/src/theme/color_theme.dart | 286 +++ .../lib/src/theme/gallery_footer_theme.dart | 217 +++ .../lib/src/theme/gallery_header_theme.dart | 179 ++ .../lib/src/theme/message_input_theme.dart | 110 ++ .../src/theme/message_list_view_theme.dart | 111 ++ .../theme/message_search_list_view_theme.dart | 112 ++ .../lib/src/theme/message_theme.dart | 103 + .../lib/src/theme/text_theme.dart | 163 ++ .../lib/src/theme/themes.dart | 15 + .../lib/src/theme/user_list_view_theme.dart | 111 ++ .../lib/src/user_list_view.dart | 1 + .../lib/stream_chat_flutter.dart | 1 + 34 files changed, 1766 insertions(+), 1702 deletions(-) create mode 100644 packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart create mode 100644 packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart create mode 100644 packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart create mode 100644 packages/stream_chat_flutter/lib/src/theme/channel_list_view_theme.dart create mode 100644 packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart create mode 100644 packages/stream_chat_flutter/lib/src/theme/channel_theme.dart create mode 100644 packages/stream_chat_flutter/lib/src/theme/color_theme.dart create mode 100644 packages/stream_chat_flutter/lib/src/theme/gallery_footer_theme.dart create mode 100644 packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart create mode 100644 packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart create mode 100644 packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart create mode 100644 packages/stream_chat_flutter/lib/src/theme/message_search_list_view_theme.dart create mode 100644 packages/stream_chat_flutter/lib/src/theme/message_theme.dart create mode 100644 packages/stream_chat_flutter/lib/src/theme/text_theme.dart create mode 100644 packages/stream_chat_flutter/lib/src/theme/themes.dart create mode 100644 packages/stream_chat_flutter/lib/src/theme/user_list_view_theme.dart diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart index b068a8f9..d0f45aad 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; diff --git a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart index 20174567..f912f282 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart @@ -4,6 +4,7 @@ import 'package:shimmer/shimmer.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_title.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_upload_state_builder.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; diff --git a/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart index b743cda1..b976532a 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_title.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart'; import 'package:stream_chat_flutter/src/full_screen_media.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index 32dde4b6..ba086f41 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -5,6 +5,7 @@ import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:shimmer/shimmer.dart'; import 'package:stream_chat_flutter/src/channel_bottom_sheet.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; diff --git a/packages/stream_chat_flutter/lib/src/deleted_message.dart b/packages/stream_chat_flutter/lib/src/deleted_message.dart index 011b0c72..a973deb7 100644 --- a/packages/stream_chat_flutter/lib/src/deleted_message.dart +++ b/packages/stream_chat_flutter/lib/src/deleted_message.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; /// Widget to display deleted message class DeletedMessage extends StatelessWidget { diff --git a/packages/stream_chat_flutter/lib/src/gallery_footer.dart b/packages/stream_chat_flutter/lib/src/gallery_footer.dart index c26c8dbc..53b20675 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_footer.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_footer.dart @@ -8,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; diff --git a/packages/stream_chat_flutter/lib/src/gallery_header.dart b/packages/stream_chat_flutter/lib/src/gallery_header.dart index 6c4ecfee..ad2969cb 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_header.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_header.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/attachment_actions_modal.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/theme/themes.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Header/AppBar widget for media display screen diff --git a/packages/stream_chat_flutter/lib/src/image_group.dart b/packages/stream_chat_flutter/lib/src/image_group.dart index 4647fe57..ec50f294 100644 --- a/packages/stream_chat_flutter/lib/src/image_group.dart +++ b/packages/stream_chat_flutter/lib/src/image_group.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/full_screen_media.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart index 128918ad..efaf0ded 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -6,6 +6,7 @@ import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/message_action.dart'; import 'package:stream_chat_flutter/src/reaction_picker.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 635e86c1..e7279579 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -14,6 +14,7 @@ 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/swipeable.dart'; import 'package:stream_chat_flutter/src/system_message.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:visibility_detector/visibility_detector.dart'; diff --git a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart index fadef6ff..669af376 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.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/theme/themes.dart'; import 'package:stream_chat_flutter/src/user_avatar.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; diff --git a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart index 13f5ea55..59e09507 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/info_tile.dart'; import 'package:stream_chat_flutter/src/message_search_item.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; diff --git a/packages/stream_chat_flutter/lib/src/message_text.dart b/packages/stream_chat_flutter/lib/src/message_text.dart index 6d006085..c11dc2fc 100644 --- a/packages/stream_chat_flutter/lib/src/message_text.dart +++ b/packages/stream_chat_flutter/lib/src/message_text.dart @@ -1,6 +1,7 @@ import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index e73f3a6d..133b5dc7 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -14,6 +14,7 @@ import 'package:stream_chat_flutter/src/message_actions_modal.dart'; import 'package:stream_chat_flutter/src/message_reactions_modal.dart'; import 'package:stream_chat_flutter/src/quoted_message_widget.dart'; import 'package:stream_chat_flutter/src/reaction_bubble.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/src/url_attachment.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; diff --git a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart index 22ca4d4c..5cf53a65 100644 --- a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart @@ -1,6 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:video_player/video_player.dart'; diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index 9d2df266..4336325b 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -1,11 +1,10 @@ import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/channel_header.dart'; +import 'package:flutter/material.dart' hide TextTheme; import 'package:stream_chat_flutter/src/channel_preview.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/gradient_avatar.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/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; @@ -492,1702 +491,3 @@ class StreamChatThemeData { } } -/// Class for holding text theme -class TextTheme { - /// Initialise light text theme - 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, - ), - }); - - /// Initialise with dark theme - 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, - ), - }); - - /// Text theme for title - final TextStyle title; - - /// Body Text theme for headline - final TextStyle headlineBold; - - /// Text theme for headline - final TextStyle headline; - - /// Bold Text theme for body - final TextStyle bodyBold; - - /// Text theme body - final TextStyle body; - - /// Bold Text theme for footnote - final TextStyle footnoteBold; - - /// Text theme for footnote - final TextStyle footnote; - - /// Bold Text theme for caption - final TextStyle captionBold; - - /// Copy with theme - TextTheme copyWith({ - Brightness brightness = Brightness.light, - TextStyle? body, - TextStyle? title, - TextStyle? headlineBold, - TextStyle? headline, - TextStyle? bodyBold, - TextStyle? footnoteBold, - TextStyle? footnote, - TextStyle? captionBold, - }) => - brightness == Brightness.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, - ); - - /// Merge text theme - TextTheme merge(TextTheme? other) { - if (other == null) return this; - return copyWith( - body: body.merge(other.body), - title: title.merge(other.title), - headlineBold: headlineBold.merge(other.headlineBold), - headline: headline.merge(other.headline), - bodyBold: bodyBold.merge(other.bodyBold), - footnoteBold: footnoteBold.merge(other.footnoteBold), - footnote: footnote.merge(other.footnote), - captionBold: captionBold.merge(other.captionBold), - ); - } -} - -/// Theme that holds colors -class ColorTheme { - /// Initialise with light theme - ColorTheme.light({ - this.textHighEmphasis = const Color(0xff000000), - this.textLowEmphasis = const Color(0xff7a7a7a), - this.disabled = const Color(0xffdbdbdb), - this.borders = const Color(0xffecebeb), - this.inputBg = const Color(0xfff2f2f2), - this.appBg = const Color(0xfffcfcfc), - this.barsBg = const Color(0xffffffff), - this.linkBg = const Color(0xffe9f2ff), - this.accentPrimary = const Color(0xff005FFF), - this.accentError = const Color(0xffFF3842), - this.accentInfo = 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: [Color(0xfff7f7f7), Color(0xfffcfcfc)], - stops: [0, 1], - ), - this.borderTop = const Effect( - sigmaX: 0, sigmaY: -1, color: Color(0xff000000), blur: 0, alpha: 0.08), - this.borderBottom = const Effect( - sigmaX: 0, sigmaY: 1, color: Color(0xff000000), blur: 0, alpha: 0.08), - this.shadowIconButton = const Effect( - sigmaX: 0, sigmaY: 2, color: Color(0xff000000), alpha: 0.5, blur: 4), - this.modalShadow = const Effect( - sigmaX: 0, sigmaY: 0, color: Color(0xff000000), alpha: 1, blur: 8), - }) : brightness = Brightness.light; - - /// Initialise with dark theme - ColorTheme.dark({ - this.textHighEmphasis = const Color(0xffffffff), - this.textLowEmphasis = const Color(0xff7a7a7a), - this.disabled = const Color(0xff2d2f2f), - this.borders = const Color(0xff1c1e22), - this.inputBg = const Color(0xff13151b), - this.appBg = const Color(0xff070A0D), - this.barsBg = const Color(0xff101418), - this.linkBg = const Color(0xff00193D), - this.accentPrimary = const Color(0xff005FFF), - this.accentError = const Color(0xffFF3742), - this.accentInfo = const Color(0xff20E070), - this.borderTop = const Effect( - sigmaX: 0, - sigmaY: -1, - color: Color(0xff141924), - blur: 0, - ), - this.borderBottom = const Effect( - sigmaX: 0, - sigmaY: 1, - color: Color(0xff141924), - blur: 0, - alpha: 1, - ), - this.shadowIconButton = const Effect( - sigmaX: 0, - sigmaY: 2, - color: Color(0xff000000), - alpha: 0.5, - blur: 4, - ), - this.modalShadow = const Effect( - sigmaX: 0, - sigmaY: 0, - color: Color(0xff000000), - alpha: 1, - blur: 8, - ), - 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: [ - Color(0xff101214), - Color(0xff070a0d), - ], - stops: [0, 1], - ), - }) : brightness = Brightness.dark; - - /// - final Color textHighEmphasis; - - /// - final Color textLowEmphasis; - - /// - final Color disabled; - - /// - final Color borders; - - /// - final Color inputBg; - - /// - final Color appBg; - - /// - final Color barsBg; - - /// - final Color linkBg; - - /// - final Color accentPrimary; - - /// - final Color accentError; - - /// - final Color accentInfo; - - /// - final Effect borderTop; - - /// - final Effect borderBottom; - - /// - final Effect shadowIconButton; - - /// - final Effect modalShadow; - - /// - final Color highlight; - - /// - final Color overlay; - - /// - final Color overlayDark; - - /// - final Gradient bgGradient; - - /// - final Brightness brightness; - - /// Copy with theme - ColorTheme copyWith({ - Brightness brightness = Brightness.light, - Color? textHighEmphasis, - Color? textLowEmphasis, - Color? disabled, - Color? borders, - Color? inputBg, - Color? appBg, - Color? barsBg, - Color? linkBg, - Color? accentPrimary, - Color? accentError, - Color? accentInfo, - Effect? borderTop, - Effect? borderBottom, - Effect? shadowIconButton, - Effect? modalShadow, - Color? highlight, - Color? overlay, - Color? overlayDark, - Gradient? bgGradient, - }) => - brightness == Brightness.light - ? ColorTheme.light( - textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis, - textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis, - disabled: disabled ?? this.disabled, - borders: borders ?? this.borders, - inputBg: inputBg ?? this.inputBg, - appBg: appBg ?? this.appBg, - barsBg: barsBg ?? this.barsBg, - linkBg: linkBg ?? this.linkBg, - accentPrimary: accentPrimary ?? this.accentPrimary, - accentError: accentError ?? this.accentError, - accentInfo: accentInfo ?? this.accentInfo, - 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( - textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis, - textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis, - disabled: disabled ?? this.disabled, - borders: borders ?? this.borders, - inputBg: inputBg ?? this.inputBg, - appBg: appBg ?? this.appBg, - barsBg: barsBg ?? this.barsBg, - linkBg: linkBg ?? this.linkBg, - accentPrimary: accentPrimary ?? this.accentPrimary, - accentError: accentError ?? this.accentError, - accentInfo: accentInfo ?? this.accentInfo, - 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, - ); - - /// Merge color theme - ColorTheme merge(ColorTheme? other) { - if (other == null) return this; - return copyWith( - textHighEmphasis: other.textHighEmphasis, - textLowEmphasis: other.textLowEmphasis, - disabled: other.disabled, - borders: other.borders, - inputBg: other.inputBg, - appBg: other.appBg, - barsBg: other.barsBg, - linkBg: other.linkBg, - accentPrimary: other.accentPrimary, - accentError: other.accentError, - accentInfo: other.accentInfo, - 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 { - /// Constructor for creating [ChannelTheme] - ChannelTheme({ - required this.channelHeaderTheme, - }); - - /// Theme of the [ChannelHeader] widget - final ChannelHeaderTheme channelHeaderTheme; - - /// Creates a copy of [ChannelTheme] with specified attributes overridden. - ChannelTheme copyWith({ - ChannelHeaderTheme? channelHeaderTheme, - }) => - ChannelTheme( - channelHeaderTheme: channelHeaderTheme ?? this.channelHeaderTheme, - ); - - /// Merge with theme - ChannelTheme merge(ChannelTheme? other) { - if (other == null) return this; - return copyWith( - channelHeaderTheme: channelHeaderTheme.merge(other.channelHeaderTheme), - ); - } -} - -/// Theme for avatar -class AvatarTheme { - /// Constructor for creating [AvatarTheme] - AvatarTheme({ - BoxConstraints? constraints, - BorderRadius? borderRadius, - }) : _constraints = constraints, - _borderRadius = borderRadius; - - final BoxConstraints? _constraints; - final BorderRadius? _borderRadius; - - /// Get constraints for avatar - BoxConstraints get constraints => - _constraints ?? - const BoxConstraints.tightFor( - height: 32, - width: 32, - ); - - /// Get border radius - BorderRadius get borderRadius => _borderRadius ?? BorderRadius.circular(20); - - /// Copy with another theme - AvatarTheme copyWith({ - BoxConstraints? constraints, - BorderRadius? borderRadius, - }) => - AvatarTheme( - constraints: constraints ?? _constraints, - borderRadius: borderRadius ?? _borderRadius, - ); - - /// Merge with another AvatarTheme - AvatarTheme merge(AvatarTheme? other) { - if (other == null) return this; - return copyWith( - constraints: other._constraints, - borderRadius: other._borderRadius, - ); - } -} - -/// Class for getting message theme -class MessageTheme { - /// Constructor into [MessageTheme] - const MessageTheme({ - this.replies, - this.messageText, - this.messageAuthor, - this.messageLinks, - this.messageBackgroundColor, - this.messageBorderColor, - this.reactionsBackgroundColor, - this.reactionsBorderColor, - this.reactionsMaskColor, - this.avatarTheme, - this.createdAt, - }); - - /// Text style for message text - final TextStyle? messageText; - - /// Text style for message author - final TextStyle? messageAuthor; - - /// Text style for message links - final TextStyle? messageLinks; - - /// Text style for created at text - final TextStyle? createdAt; - - /// Text style for replies - final TextStyle? replies; - - /// Color for messageBackgroundColor - final Color? messageBackgroundColor; - - /// Color for message border color - final Color? messageBorderColor; - - /// Color for reactions - final Color? reactionsBackgroundColor; - - /// Colors reaction border - final Color? reactionsBorderColor; - - /// Color for reaction mask - final Color? reactionsMaskColor; - - /// Theme of the avatar - final AvatarTheme? avatarTheme; - - /// Copy with a theme - MessageTheme copyWith({ - TextStyle? messageText, - TextStyle? messageAuthor, - TextStyle? messageLinks, - TextStyle? createdAt, - TextStyle? replies, - Color? messageBackgroundColor, - Color? messageBorderColor, - AvatarTheme? avatarTheme, - Color? reactionsBackgroundColor, - Color? reactionsBorderColor, - Color? reactionsMaskColor, - }) => - 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, - reactionsMaskColor: reactionsMaskColor ?? this.reactionsMaskColor, - ); - - /// Merge with a theme - 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, - reactionsMaskColor: other.reactionsMaskColor, - ); - } -} - -/// Theme for channel preview -class ChannelPreviewTheme { - /// Constructor for creating [ChannelPreviewTheme] - const ChannelPreviewTheme({ - this.title, - this.subtitle, - this.lastMessageAt, - this.avatarTheme, - this.unreadCounterColor, - this.indicatorIconSize, - }); - - /// Theme for title - final TextStyle? title; - - /// Theme for subtitle - final TextStyle? subtitle; - - /// Theme of last message at - final TextStyle? lastMessageAt; - - /// Avatar theme - final AvatarTheme? avatarTheme; - - /// Unread counter color - final Color? unreadCounterColor; - - /// Indicator icon size - final double? indicatorIconSize; - - /// Copy with theme - 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, - ); - - /// Merge with theme - 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, - ); - } -} - -/// Theme for [ChannelHeader] -class ChannelHeaderTheme { - /// Constructor for creating a [ChannelHeaderTheme] - const ChannelHeaderTheme({ - this.title, - this.subtitle, - this.avatarTheme, - this.color, - }); - - /// Theme for title - final TextStyle? title; - - /// Theme for subtitle - final TextStyle? subtitle; - - /// Theme for avatar - final AvatarTheme? avatarTheme; - - /// Color for [ChannelHeaderTheme] - final Color? color; - - /// Copy with theme - ChannelHeaderTheme copyWith({ - TextStyle? title, - TextStyle? subtitle, - AvatarTheme? avatarTheme, - Color? color, - }) => - ChannelHeaderTheme( - title: title ?? this.title, - subtitle: subtitle ?? this.subtitle, - avatarTheme: avatarTheme ?? this.avatarTheme, - color: color ?? this.color, - ); - - /// Merge with other [ChannelHeaderTheme] - ChannelHeaderTheme merge(ChannelHeaderTheme? other) { - if (other == null) return this; - return copyWith( - title: title?.merge(other.title) ?? other.title, - subtitle: subtitle?.merge(other.subtitle) ?? other.subtitle, - avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme, - color: other.color, - ); - } -} - -/// Theme dedicated to the [ChannelListHeader] -class ChannelListHeaderTheme { - /// Returns a new [ChannelListHeaderTheme] - const ChannelListHeaderTheme({ - this.title, - this.avatarTheme, - this.color, - }); - - /// Style of the title text - final TextStyle? title; - - /// Theme dedicated to the userAvatar - final AvatarTheme? avatarTheme; - - /// Background color of the appbar - final Color? color; - - /// Returns a new [ChannelListHeaderTheme] replacing some of its properties - ChannelListHeaderTheme copyWith({ - TextStyle? title, - AvatarTheme? avatarTheme, - Color? color, - }) => - ChannelListHeaderTheme( - title: title ?? this.title, - avatarTheme: avatarTheme ?? this.avatarTheme, - color: color ?? this.color, - ); - - /// Merges [this] [ChannelListHeaderTheme] with the [other] - ChannelListHeaderTheme merge(ChannelListHeaderTheme? other) { - if (other == null) return this; - return copyWith( - title: title?.merge(other.title) ?? other.title, - avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme, - color: other.color, - ); - } -} - -/// Defines the theme dedicated to the [MessageInput] widget -class MessageInputTheme { - /// Returns a new [MessageInputTheme] - const MessageInputTheme({ - this.sendAnimationDuration, - this.actionButtonColor, - this.sendButtonColor, - this.actionButtonIdleColor, - this.sendButtonIdleColor, - this.inputBackground, - this.inputTextStyle, - this.inputDecoration, - this.activeBorderGradient, - this.idleBorderGradient, - this.borderRadius, - this.expandButtonColor, - }); - - /// Duration of the [MessageInput] send button animation - final Duration? sendAnimationDuration; - - /// Background color of [MessageInput] send button - final Color? sendButtonColor; - - /// Background color of [MessageInput] action buttons - final Color? actionButtonColor; - - /// Background color of [MessageInput] send button - final Color? sendButtonIdleColor; - - /// Background color of [MessageInput] action buttons - final Color? actionButtonIdleColor; - - /// Background color of [MessageInput] expand button - final Color? expandButtonColor; - - /// Background color of [MessageInput] - final Color? inputBackground; - - /// TextStyle of [MessageInput] - final TextStyle? inputTextStyle; - - /// InputDecoration of [MessageInput] - final InputDecoration? inputDecoration; - - /// Border gradient when the [MessageInput] is not focused - final Gradient? idleBorderGradient; - - /// Border gradient when the [MessageInput] is focused - final Gradient? activeBorderGradient; - - /// Border radius of [MessageInput] - final BorderRadius? borderRadius; - - /// Returns a new [MessageInputTheme] replacing some of its properties - MessageInputTheme copyWith({ - Duration? sendAnimationDuration, - Color? inputBackground, - Color? actionButtonColor, - Color? sendButtonColor, - Color? actionButtonIdleColor, - Color? sendButtonIdleColor, - Color? expandButtonColor, - TextStyle? inputTextStyle, - InputDecoration? inputDecoration, - Gradient? activeBorderGradient, - Gradient? idleBorderGradient, - BorderRadius? borderRadius, - }) => - MessageInputTheme( - sendAnimationDuration: - sendAnimationDuration ?? this.sendAnimationDuration, - inputBackground: inputBackground ?? this.inputBackground, - actionButtonColor: actionButtonColor ?? this.actionButtonColor, - sendButtonColor: sendButtonColor ?? this.sendButtonColor, - actionButtonIdleColor: - actionButtonIdleColor ?? this.actionButtonIdleColor, - expandButtonColor: expandButtonColor ?? this.expandButtonColor, - inputTextStyle: inputTextStyle ?? this.inputTextStyle, - sendButtonIdleColor: sendButtonIdleColor ?? this.sendButtonIdleColor, - inputDecoration: inputDecoration ?? this.inputDecoration, - activeBorderGradient: activeBorderGradient ?? this.activeBorderGradient, - idleBorderGradient: idleBorderGradient ?? this.idleBorderGradient, - borderRadius: borderRadius ?? this.borderRadius, - ); - - /// Merges [this] [MessageInputTheme] with the [other] - MessageInputTheme merge(MessageInputTheme? other) { - if (other == null) return this; - return copyWith( - sendAnimationDuration: other.sendAnimationDuration, - inputBackground: other.inputBackground, - actionButtonColor: other.actionButtonColor, - actionButtonIdleColor: other.actionButtonIdleColor, - sendButtonColor: other.sendButtonColor, - sendButtonIdleColor: other.sendButtonIdleColor, - inputTextStyle: - inputTextStyle?.merge(other.inputTextStyle) ?? other.inputTextStyle, - inputDecoration: inputDecoration?.merge(other.inputDecoration) ?? - other.inputDecoration, - activeBorderGradient: other.activeBorderGradient, - idleBorderGradient: other.idleBorderGradient, - borderRadius: other.borderRadius, - expandButtonColor: other.expandButtonColor, - ); - } -} - -/// Effect store -class Effect { - /// Constructor for creating [Effect] - const Effect({ - this.sigmaX, - this.sigmaY, - this.color, - this.alpha, - this.blur, - }); - - /// - final double? sigmaX; - - /// - final double? sigmaY; - - /// - final Color? color; - - /// - final double? alpha; - - /// - final double? blur; - - /// Copy with new effect - 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 as double? ?? this.alpha, - 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(); - 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(); - 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 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)); - } -} - -/// Overrides the default style of [MessageListView] descendants. -/// -/// See also: -/// -/// * [MessageListViewThemeData], which is used to configure this theme. -class MessageListViewTheme extends InheritedTheme { - /// Creates a [MessageListViewTheme]. - /// - /// The [data] parameter must not be null. - const MessageListViewTheme({ - Key? key, - required this.data, - required Widget child, - }) : super(key: key, child: child); - - /// The configuration of this theme. - final MessageListViewThemeData data; - - /// The closest instance of this class that encloses the given context. - /// - /// If there is no enclosing [MessageListViewTheme] widget, then - /// [StreamChatThemeData.messageListViewTheme] is used. - /// - /// Typical usage is as follows: - /// - /// ```dart - /// MessageListViewTheme theme = MessageListViewTheme.of(context); - /// ``` - static MessageListViewThemeData of(BuildContext context) { - final messageListViewTheme = - context.dependOnInheritedWidgetOfExactType(); - return messageListViewTheme?.data ?? - StreamChatTheme.of(context).messageListViewTheme; - } - - @override - Widget wrap(BuildContext context, Widget child) => - MessageListViewTheme(data: data, child: child); - - @override - bool updateShouldNotify(MessageListViewTheme oldWidget) => - data != oldWidget.data; -} - -/// A style that overrides the default appearance of [MessageListView]s when -/// used with [MessageListViewTheme] or with the overall [StreamChatTheme]'s -/// [StreamChatThemeData.messageListViewTheme]. -/// -/// See also: -/// -/// * [MessageListViewTheme], the theme which is configured with this class. -/// * [StreamChatThemeData.messageListViewTheme], which can be used to override -/// the default style for [MessageListView]s below the overall -/// [StreamChatTheme]. -class MessageListViewThemeData with Diagnosticable { - /// Creates a [MessageListViewThemeData]. - const MessageListViewThemeData({ - this.backgroundColor, - }); - - /// The color of the [MessageListView] background. - final Color? backgroundColor; - - /// Copies this [MessageListViewThemeData] to another. - MessageListViewThemeData copyWith({ - Color? backgroundColor, - }) => - MessageListViewThemeData( - backgroundColor: backgroundColor ?? this.backgroundColor, - ); - - /// Linearly interpolate between two [MessageListView] themes. - /// - /// All the properties must be non-null. - MessageListViewThemeData lerp( - MessageListViewThemeData a, - MessageListViewThemeData b, - double t, - ) => - MessageListViewThemeData( - backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), - ); - - /// Merges one [MessageListViewThemeData] with another. - MessageListViewThemeData merge(MessageListViewThemeData? other) { - if (other == null) return this; - return copyWith( - backgroundColor: other.backgroundColor, - ); - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is MessageListViewThemeData && - runtimeType == other.runtimeType && - backgroundColor == other.backgroundColor; - - @override - int get hashCode => backgroundColor.hashCode; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(ColorProperty('backgroundColor', backgroundColor)); - } -} - -/// Overrides the default style of [ChannelListView] descendants. -/// -/// See also: -/// -/// * [ChannelListViewThemeData], which is used to configure this theme. -class ChannelListViewTheme extends InheritedTheme { - /// Creates a [ChannelListViewTheme]. - /// - /// The [data] parameter must not be null. - const ChannelListViewTheme({ - Key? key, - required this.data, - required Widget child, - }) : super(key: key, child: child); - - /// The configuration of this theme. - final ChannelListViewThemeData data; - - /// The closest instance of this class that encloses the given context. - /// - /// If there is no enclosing [ChannelListViewTheme] widget, then - /// [StreamChatThemeData.channelListViewTheme] is used. - /// - /// Typical usage is as follows: - /// - /// ```dart - /// ChannelListViewTheme theme = ChannelListViewTheme.of(context); - /// ``` - static ChannelListViewThemeData of(BuildContext context) { - final channelListViewTheme = - context.dependOnInheritedWidgetOfExactType(); - return channelListViewTheme?.data ?? - StreamChatTheme.of(context).channelListViewTheme; - } - - @override - Widget wrap(BuildContext context, Widget child) => - ChannelListViewTheme(data: data, child: child); - - @override - bool updateShouldNotify(ChannelListViewTheme oldWidget) => - data != oldWidget.data; -} - -/// A style that overrides the default appearance of [ChannelListView]s when -/// used with [ChannelListViewTheme] or with the overall [StreamChatTheme]'s -/// [StreamChatThemeData.channelListViewTheme]. -/// -/// See also: -/// -/// * [ChannelListViewTheme], the theme which is configured with this class. -/// * [StreamChatThemeData.channelListViewTheme], which can be used to override -/// the default style for [ChannelListView]s below the overall -/// [StreamChatTheme]. -class ChannelListViewThemeData with Diagnosticable { - /// Creates a [ChannelListViewThemeData]. - const ChannelListViewThemeData({ - this.backgroundColor, - }); - - /// The color of the [ChannelListView] background. - final Color? backgroundColor; - - /// Copies this [ChannelListViewThemeData] to another. - ChannelListViewThemeData copyWith({ - Color? backgroundColor, - }) => - ChannelListViewThemeData( - backgroundColor: backgroundColor ?? this.backgroundColor, - ); - - /// Linearly interpolate between two [ChannelListViewThemeData] themes. - /// - /// All the properties must be non-null. - ChannelListViewThemeData lerp( - ChannelListViewThemeData a, - ChannelListViewThemeData b, - double t, - ) => - ChannelListViewThemeData( - backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), - ); - - /// Merges one [ChannelListViewThemeData] with another. - ChannelListViewThemeData merge(ChannelListViewThemeData? other) { - if (other == null) return this; - return copyWith( - backgroundColor: other.backgroundColor, - ); - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ChannelListViewThemeData && - runtimeType == other.runtimeType && - backgroundColor == other.backgroundColor; - - @override - int get hashCode => backgroundColor.hashCode; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(ColorProperty('backgroundColor', backgroundColor)); - } -} - -/// Overrides the default style of [UserListView] descendants. -/// -/// See also: -/// -/// * [UserListViewThemeData], which is used to configure this theme. -class UserListViewTheme extends InheritedTheme { - /// Creates a [UserListViewTheme]. - /// - /// The [data] parameter must not be null. - const UserListViewTheme({ - Key? key, - required this.data, - required Widget child, - }) : super(key: key, child: child); - - /// The configuration of this theme. - final UserListViewThemeData data; - - /// The closest instance of this class that encloses the given context. - /// - /// If there is no enclosing [UserListViewTheme] widget, then - /// [StreamChatThemeData.userListViewTheme] is used. - /// - /// Typical usage is as follows: - /// - /// ```dart - /// UserListViewTheme theme = UserListViewTheme.of(context); - /// ``` - static UserListViewThemeData of(BuildContext context) { - final userListViewTheme = - context.dependOnInheritedWidgetOfExactType(); - return userListViewTheme?.data ?? - StreamChatTheme.of(context).userListViewTheme; - } - - @override - Widget wrap(BuildContext context, Widget child) => - UserListViewTheme(data: data, child: child); - - @override - bool updateShouldNotify(UserListViewTheme oldWidget) => - data != oldWidget.data; -} - -/// A style that overrides the default appearance of [UserListView]s when -/// used with [UserListViewTheme] or with the overall [StreamChatTheme]'s -/// [StreamChatThemeData.userListViewTheme]. -/// -/// See also: -/// -/// * [UserListViewTheme], the theme which is configured with this class. -/// * [StreamChatThemeData.userListViewTheme], which can be used to override -/// the default style for [UserListView]s below the overall -/// [StreamChatTheme]. -class UserListViewThemeData with Diagnosticable { - /// Creates a [UserListViewThemeData]. - const UserListViewThemeData({ - this.backgroundColor, - }); - - /// The color of the [ChannelListView] background. - final Color? backgroundColor; - - /// Copies this [ChannelListViewThemeData] to another. - UserListViewThemeData copyWith({ - Color? backgroundColor, - }) => - UserListViewThemeData( - backgroundColor: backgroundColor ?? this.backgroundColor, - ); - - /// Linearly interpolate between two [UserListViewThemeData] themes. - /// - /// All the properties must be non-null. - UserListViewThemeData lerp( - UserListViewThemeData a, - UserListViewThemeData b, - double t, - ) => - UserListViewThemeData( - backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), - ); - - /// Merges one [UserListViewThemeData] with another. - UserListViewThemeData merge(UserListViewThemeData? other) { - if (other == null) return this; - return copyWith( - backgroundColor: other.backgroundColor, - ); - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is UserListViewThemeData && - runtimeType == other.runtimeType && - backgroundColor == other.backgroundColor; - - @override - int get hashCode => backgroundColor.hashCode; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(ColorProperty('backgroundColor', backgroundColor)); - } -} - -/// Overrides the default style of [MessageSearchListView] descendants. -/// -/// See also: -/// -/// * [UserListViewThemeData], which is used to configure this theme. -class MessageSearchListViewTheme extends InheritedTheme { - /// Creates a [UserListViewTheme]. - /// - /// The [data] parameter must not be null. - const MessageSearchListViewTheme({ - Key? key, - required this.data, - required Widget child, - }) : super(key: key, child: child); - - /// The configuration of this theme. - final MessageSearchListViewThemeData data; - - /// The closest instance of this class that encloses the given context. - /// - /// If there is no enclosing [MessageSearchListView] widget, then - /// [StreamChatThemeData.messageSearchListViewTheme] is used. - /// - /// Typical usage is as follows: - /// - /// ```dart - /// MessageSearchListViewTheme theme = MessageSearchListViewTheme.of(context); - /// ``` - static MessageSearchListViewThemeData of(BuildContext context) { - final messageSearchListViewTheme = context - .dependOnInheritedWidgetOfExactType(); - return messageSearchListViewTheme?.data ?? - StreamChatTheme.of(context).messageSearchListViewTheme; - } - - @override - Widget wrap(BuildContext context, Widget child) => - MessageSearchListViewTheme(data: data, child: child); - - @override - bool updateShouldNotify(MessageSearchListViewTheme oldWidget) => - data != oldWidget.data; -} - -/// A style that overrides the default appearance of [MessageSearchListView]s -/// when used with [MessageSearchListView] or with the overall -/// [StreamChatTheme]'s [StreamChatThemeData.messageSearchListViewTheme]. -/// -/// See also: -/// -/// * [MessageSearchListViewTheme], the theme which is configured with this -/// class. -/// * [StreamChatThemeData.messageSearchListViewTheme], which can be used to -/// override the default style for [UserListView]s below the overall -/// [StreamChatTheme]. -class MessageSearchListViewThemeData with Diagnosticable { - /// Creates a [MessageSearchListViewThemeData]. - const MessageSearchListViewThemeData({ - this.backgroundColor, - }); - - /// The color of the [MessageSearchListView] background. - final Color? backgroundColor; - - /// Copies this [MessageSearchListViewThemeData] to another. - MessageSearchListViewThemeData copyWith({ - Color? backgroundColor, - }) => - MessageSearchListViewThemeData( - backgroundColor: backgroundColor ?? this.backgroundColor, - ); - - /// Linearly interpolate between two [UserListViewThemeData] themes. - /// - /// All the properties must be non-null. - MessageSearchListViewThemeData lerp( - MessageSearchListViewThemeData a, - MessageSearchListViewThemeData b, - double t, - ) => - MessageSearchListViewThemeData( - backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), - ); - - /// Merges one [MessageSearchListViewThemeData] with another. - MessageSearchListViewThemeData merge(MessageSearchListViewThemeData? other) { - if (other == null) return this; - return copyWith( - backgroundColor: other.backgroundColor, - ); - } - - @override - bool operator ==(Object other) => - identical(this, other) || - other is MessageSearchListViewThemeData && - runtimeType == other.runtimeType && - backgroundColor == other.backgroundColor; - - @override - int get hashCode => backgroundColor.hashCode; - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(ColorProperty('backgroundColor', backgroundColor)); - } -} diff --git a/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart b/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart new file mode 100644 index 00000000..f3e68597 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +/// Theme for avatar +class AvatarTheme { + /// Constructor for creating [AvatarTheme] + AvatarTheme({ + BoxConstraints? constraints, + BorderRadius? borderRadius, + }) : _constraints = constraints, + _borderRadius = borderRadius; + + final BoxConstraints? _constraints; + final BorderRadius? _borderRadius; + + /// Get constraints for avatar + BoxConstraints get constraints => + _constraints ?? + const BoxConstraints.tightFor( + height: 32, + width: 32, + ); + + /// Get border radius + BorderRadius get borderRadius => _borderRadius ?? BorderRadius.circular(20); + + /// Copy with another theme + AvatarTheme copyWith({ + BoxConstraints? constraints, + BorderRadius? borderRadius, + }) => + AvatarTheme( + constraints: constraints ?? _constraints, + borderRadius: borderRadius ?? _borderRadius, + ); + + /// Merge with another AvatarTheme + AvatarTheme merge(AvatarTheme? other) { + if (other == null) return this; + return copyWith( + constraints: other._constraints, + borderRadius: other._borderRadius, + ); + } +} \ No newline at end of file diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart new file mode 100644 index 00000000..2072b3c5 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; + +/// Theme for [ChannelHeader] +class ChannelHeaderTheme { + /// Constructor for creating a [ChannelHeaderTheme] + const ChannelHeaderTheme({ + this.title, + this.subtitle, + this.avatarTheme, + this.color, + }); + + /// Theme for title + final TextStyle? title; + + /// Theme for subtitle + final TextStyle? subtitle; + + /// Theme for avatar + final AvatarTheme? avatarTheme; + + /// Color for [ChannelHeaderTheme] + final Color? color; + + /// Copy with theme + ChannelHeaderTheme copyWith({ + TextStyle? title, + TextStyle? subtitle, + AvatarTheme? avatarTheme, + Color? color, + }) => + ChannelHeaderTheme( + title: title ?? this.title, + subtitle: subtitle ?? this.subtitle, + avatarTheme: avatarTheme ?? this.avatarTheme, + color: color ?? this.color, + ); + + /// Merge with other [ChannelHeaderTheme] + ChannelHeaderTheme merge(ChannelHeaderTheme? other) { + if (other == null) return this; + return copyWith( + title: title?.merge(other.title) ?? other.title, + subtitle: subtitle?.merge(other.subtitle) ?? other.subtitle, + avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme, + color: other.color, + ); + } +} \ No newline at end of file diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart new file mode 100644 index 00000000..ca36b9bd --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; + +/// Theme dedicated to the [ChannelListHeader] +class ChannelListHeaderTheme { + /// Returns a new [ChannelListHeaderTheme] + const ChannelListHeaderTheme({ + this.title, + this.avatarTheme, + this.color, + }); + + /// Style of the title text + final TextStyle? title; + + /// Theme dedicated to the userAvatar + final AvatarTheme? avatarTheme; + + /// Background color of the appbar + final Color? color; + + /// Returns a new [ChannelListHeaderTheme] replacing some of its properties + ChannelListHeaderTheme copyWith({ + TextStyle? title, + AvatarTheme? avatarTheme, + Color? color, + }) => + ChannelListHeaderTheme( + title: title ?? this.title, + avatarTheme: avatarTheme ?? this.avatarTheme, + color: color ?? this.color, + ); + + /// Merges [this] [ChannelListHeaderTheme] with the [other] + ChannelListHeaderTheme merge(ChannelListHeaderTheme? other) { + if (other == null) return this; + return copyWith( + title: title?.merge(other.title) ?? other.title, + avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme, + color: other.color, + ); + } +} \ No newline at end of file diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_list_view_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_list_view_theme.dart new file mode 100644 index 00000000..d310e2bc --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/channel_list_view_theme.dart @@ -0,0 +1,111 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; + +/// Overrides the default style of [ChannelListView] descendants. +/// +/// See also: +/// +/// * [ChannelListViewThemeData], which is used to configure this theme. +class ChannelListViewTheme extends InheritedTheme { + /// Creates a [ChannelListViewTheme]. + /// + /// The [data] parameter must not be null. + const ChannelListViewTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final ChannelListViewThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [ChannelListViewTheme] widget, then + /// [StreamChatThemeData.channelListViewTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// ChannelListViewTheme theme = ChannelListViewTheme.of(context); + /// ``` + static ChannelListViewThemeData of(BuildContext context) { + final channelListViewTheme = + context.dependOnInheritedWidgetOfExactType(); + return channelListViewTheme?.data ?? + StreamChatTheme.of(context).channelListViewTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) => + ChannelListViewTheme(data: data, child: child); + + @override + bool updateShouldNotify(ChannelListViewTheme oldWidget) => + data != oldWidget.data; +} + +/// A style that overrides the default appearance of [ChannelListView]s when +/// used with [ChannelListViewTheme] or with the overall [StreamChatTheme]'s +/// [StreamChatThemeData.channelListViewTheme]. +/// +/// See also: +/// +/// * [ChannelListViewTheme], the theme which is configured with this class. +/// * [StreamChatThemeData.channelListViewTheme], which can be used to override +/// the default style for [ChannelListView]s below the overall +/// [StreamChatTheme]. +class ChannelListViewThemeData with Diagnosticable { + /// Creates a [ChannelListViewThemeData]. + const ChannelListViewThemeData({ + this.backgroundColor, + }); + + /// The color of the [ChannelListView] background. + final Color? backgroundColor; + + /// Copies this [ChannelListViewThemeData] to another. + ChannelListViewThemeData copyWith({ + Color? backgroundColor, + }) => + ChannelListViewThemeData( + backgroundColor: backgroundColor ?? this.backgroundColor, + ); + + /// Linearly interpolate between two [ChannelListViewThemeData] themes. + /// + /// All the properties must be non-null. + ChannelListViewThemeData lerp( + ChannelListViewThemeData a, + ChannelListViewThemeData b, + double t, + ) => + ChannelListViewThemeData( + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + ); + + /// Merges one [ChannelListViewThemeData] with another. + ChannelListViewThemeData merge(ChannelListViewThemeData? other) { + if (other == null) return this; + return copyWith( + backgroundColor: other.backgroundColor, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ChannelListViewThemeData && + runtimeType == other.runtimeType && + backgroundColor == other.backgroundColor; + + @override + int get hashCode => backgroundColor.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ColorProperty('backgroundColor', backgroundColor)); + } +} diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart new file mode 100644 index 00000000..4643bb8d --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; + +/// Theme for channel preview +class ChannelPreviewTheme { + /// Constructor for creating [ChannelPreviewTheme] + const ChannelPreviewTheme({ + this.title, + this.subtitle, + this.lastMessageAt, + this.avatarTheme, + this.unreadCounterColor, + this.indicatorIconSize, + }); + + /// Theme for title + final TextStyle? title; + + /// Theme for subtitle + final TextStyle? subtitle; + + /// Theme of last message at + final TextStyle? lastMessageAt; + + /// Avatar theme + final AvatarTheme? avatarTheme; + + /// Unread counter color + final Color? unreadCounterColor; + + /// Indicator icon size + final double? indicatorIconSize; + + /// Copy with theme + 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, + ); + + /// Merge with theme + 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, + ); + } +} \ No newline at end of file diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_theme.dart new file mode 100644 index 00000000..bf752f94 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/channel_theme.dart @@ -0,0 +1,28 @@ +import 'package:stream_chat_flutter/src/theme/channel_header_theme.dart'; + +/// Channel theme data +class ChannelTheme { + /// Constructor for creating [ChannelTheme] + ChannelTheme({ + required this.channelHeaderTheme, + }); + + /// Theme of the [ChannelHeader] widget + final ChannelHeaderTheme channelHeaderTheme; + + /// Creates a copy of [ChannelTheme] with specified attributes overridden. + ChannelTheme copyWith({ + ChannelHeaderTheme? channelHeaderTheme, + }) => + ChannelTheme( + channelHeaderTheme: channelHeaderTheme ?? this.channelHeaderTheme, + ); + + /// Merge with theme + ChannelTheme merge(ChannelTheme? other) { + if (other == null) return this; + return copyWith( + channelHeaderTheme: channelHeaderTheme.merge(other.channelHeaderTheme), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/theme/color_theme.dart b/packages/stream_chat_flutter/lib/src/theme/color_theme.dart new file mode 100644 index 00000000..d021c6cf --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/color_theme.dart @@ -0,0 +1,286 @@ +import 'package:flutter/material.dart'; + +/// Theme that holds colors +class ColorTheme { + /// Initialise with light theme + ColorTheme.light({ + this.textHighEmphasis = const Color(0xff000000), + this.textLowEmphasis = const Color(0xff7a7a7a), + this.disabled = const Color(0xffdbdbdb), + this.borders = const Color(0xffecebeb), + this.inputBg = const Color(0xfff2f2f2), + this.appBg = const Color(0xfffcfcfc), + this.barsBg = const Color(0xffffffff), + this.linkBg = const Color(0xffe9f2ff), + this.accentPrimary = const Color(0xff005FFF), + this.accentError = const Color(0xffFF3842), + this.accentInfo = 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: [Color(0xfff7f7f7), Color(0xfffcfcfc)], + stops: [0, 1], + ), + this.borderTop = const Effect( + sigmaX: 0, sigmaY: -1, color: Color(0xff000000), blur: 0, alpha: 0.08), + this.borderBottom = const Effect( + sigmaX: 0, sigmaY: 1, color: Color(0xff000000), blur: 0, alpha: 0.08), + this.shadowIconButton = const Effect( + sigmaX: 0, sigmaY: 2, color: Color(0xff000000), alpha: 0.5, blur: 4), + this.modalShadow = const Effect( + sigmaX: 0, sigmaY: 0, color: Color(0xff000000), alpha: 1, blur: 8), + }) : brightness = Brightness.light; + + /// Initialise with dark theme + ColorTheme.dark({ + this.textHighEmphasis = const Color(0xffffffff), + this.textLowEmphasis = const Color(0xff7a7a7a), + this.disabled = const Color(0xff2d2f2f), + this.borders = const Color(0xff1c1e22), + this.inputBg = const Color(0xff13151b), + this.appBg = const Color(0xff070A0D), + this.barsBg = const Color(0xff101418), + this.linkBg = const Color(0xff00193D), + this.accentPrimary = const Color(0xff005FFF), + this.accentError = const Color(0xffFF3742), + this.accentInfo = const Color(0xff20E070), + this.borderTop = const Effect( + sigmaX: 0, + sigmaY: -1, + color: Color(0xff141924), + blur: 0, + ), + this.borderBottom = const Effect( + sigmaX: 0, + sigmaY: 1, + color: Color(0xff141924), + blur: 0, + alpha: 1, + ), + this.shadowIconButton = const Effect( + sigmaX: 0, + sigmaY: 2, + color: Color(0xff000000), + alpha: 0.5, + blur: 4, + ), + this.modalShadow = const Effect( + sigmaX: 0, + sigmaY: 0, + color: Color(0xff000000), + alpha: 1, + blur: 8, + ), + 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: [ + Color(0xff101214), + Color(0xff070a0d), + ], + stops: [0, 1], + ), + }) : brightness = Brightness.dark; + + /// + final Color textHighEmphasis; + + /// + final Color textLowEmphasis; + + /// + final Color disabled; + + /// + final Color borders; + + /// + final Color inputBg; + + /// + final Color appBg; + + /// + final Color barsBg; + + /// + final Color linkBg; + + /// + final Color accentPrimary; + + /// + final Color accentError; + + /// + final Color accentInfo; + + /// + final Effect borderTop; + + /// + final Effect borderBottom; + + /// + final Effect shadowIconButton; + + /// + final Effect modalShadow; + + /// + final Color highlight; + + /// + final Color overlay; + + /// + final Color overlayDark; + + /// + final Gradient bgGradient; + + /// + final Brightness brightness; + + /// Copy with theme + ColorTheme copyWith({ + Brightness brightness = Brightness.light, + Color? textHighEmphasis, + Color? textLowEmphasis, + Color? disabled, + Color? borders, + Color? inputBg, + Color? appBg, + Color? barsBg, + Color? linkBg, + Color? accentPrimary, + Color? accentError, + Color? accentInfo, + Effect? borderTop, + Effect? borderBottom, + Effect? shadowIconButton, + Effect? modalShadow, + Color? highlight, + Color? overlay, + Color? overlayDark, + Gradient? bgGradient, + }) => + brightness == Brightness.light + ? ColorTheme.light( + textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis, + textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis, + disabled: disabled ?? this.disabled, + borders: borders ?? this.borders, + inputBg: inputBg ?? this.inputBg, + appBg: appBg ?? this.appBg, + barsBg: barsBg ?? this.barsBg, + linkBg: linkBg ?? this.linkBg, + accentPrimary: accentPrimary ?? this.accentPrimary, + accentError: accentError ?? this.accentError, + accentInfo: accentInfo ?? this.accentInfo, + 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( + textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis, + textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis, + disabled: disabled ?? this.disabled, + borders: borders ?? this.borders, + inputBg: inputBg ?? this.inputBg, + appBg: appBg ?? this.appBg, + barsBg: barsBg ?? this.barsBg, + linkBg: linkBg ?? this.linkBg, + accentPrimary: accentPrimary ?? this.accentPrimary, + accentError: accentError ?? this.accentError, + accentInfo: accentInfo ?? this.accentInfo, + 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, + ); + + /// Merge color theme + ColorTheme merge(ColorTheme? other) { + if (other == null) return this; + return copyWith( + textHighEmphasis: other.textHighEmphasis, + textLowEmphasis: other.textLowEmphasis, + disabled: other.disabled, + borders: other.borders, + inputBg: other.inputBg, + appBg: other.appBg, + barsBg: other.barsBg, + linkBg: other.linkBg, + accentPrimary: other.accentPrimary, + accentError: other.accentError, + accentInfo: other.accentInfo, + highlight: other.highlight, + overlay: other.overlay, + overlayDark: other.overlayDark, + bgGradient: other.bgGradient, + borderTop: other.borderTop, + borderBottom: other.borderBottom, + shadowIconButton: other.shadowIconButton, + modalShadow: other.modalShadow, + ); + } +} + +/// Effect store +class Effect { + /// Constructor for creating [Effect] + const Effect({ + this.sigmaX, + this.sigmaY, + this.color, + this.alpha, + this.blur, + }); + + /// + final double? sigmaX; + + /// + final double? sigmaY; + + /// + final Color? color; + + /// + final double? alpha; + + /// + final double? blur; + + /// Copy with new effect + 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 as double? ?? this.alpha, + blur: blur ?? this.blur, + ); +} diff --git a/packages/stream_chat_flutter/lib/src/theme/gallery_footer_theme.dart b/packages/stream_chat_flutter/lib/src/theme/gallery_footer_theme.dart new file mode 100644 index 00000000..bdf273e7 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/gallery_footer_theme.dart @@ -0,0 +1,217 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; + +/// 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(); + 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 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)); + } +} diff --git a/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart new file mode 100644 index 00000000..9b5faa83 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart @@ -0,0 +1,179 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; + +/// 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(); + 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)); + } +} diff --git a/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart new file mode 100644 index 00000000..b8889e6b --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; + +/// Defines the theme dedicated to the [MessageInput] widget +class MessageInputTheme { + /// Returns a new [MessageInputTheme] + const MessageInputTheme({ + this.sendAnimationDuration, + this.actionButtonColor, + this.sendButtonColor, + this.actionButtonIdleColor, + this.sendButtonIdleColor, + this.inputBackground, + this.inputTextStyle, + this.inputDecoration, + this.activeBorderGradient, + this.idleBorderGradient, + this.borderRadius, + this.expandButtonColor, + }); + + /// Duration of the [MessageInput] send button animation + final Duration? sendAnimationDuration; + + /// Background color of [MessageInput] send button + final Color? sendButtonColor; + + /// Background color of [MessageInput] action buttons + final Color? actionButtonColor; + + /// Background color of [MessageInput] send button + final Color? sendButtonIdleColor; + + /// Background color of [MessageInput] action buttons + final Color? actionButtonIdleColor; + + /// Background color of [MessageInput] expand button + final Color? expandButtonColor; + + /// Background color of [MessageInput] + final Color? inputBackground; + + /// TextStyle of [MessageInput] + final TextStyle? inputTextStyle; + + /// InputDecoration of [MessageInput] + final InputDecoration? inputDecoration; + + /// Border gradient when the [MessageInput] is not focused + final Gradient? idleBorderGradient; + + /// Border gradient when the [MessageInput] is focused + final Gradient? activeBorderGradient; + + /// Border radius of [MessageInput] + final BorderRadius? borderRadius; + + /// Returns a new [MessageInputTheme] replacing some of its properties + MessageInputTheme copyWith({ + Duration? sendAnimationDuration, + Color? inputBackground, + Color? actionButtonColor, + Color? sendButtonColor, + Color? actionButtonIdleColor, + Color? sendButtonIdleColor, + Color? expandButtonColor, + TextStyle? inputTextStyle, + InputDecoration? inputDecoration, + Gradient? activeBorderGradient, + Gradient? idleBorderGradient, + BorderRadius? borderRadius, + }) => + MessageInputTheme( + sendAnimationDuration: + sendAnimationDuration ?? this.sendAnimationDuration, + inputBackground: inputBackground ?? this.inputBackground, + actionButtonColor: actionButtonColor ?? this.actionButtonColor, + sendButtonColor: sendButtonColor ?? this.sendButtonColor, + actionButtonIdleColor: + actionButtonIdleColor ?? this.actionButtonIdleColor, + expandButtonColor: expandButtonColor ?? this.expandButtonColor, + inputTextStyle: inputTextStyle ?? this.inputTextStyle, + sendButtonIdleColor: sendButtonIdleColor ?? this.sendButtonIdleColor, + inputDecoration: inputDecoration ?? this.inputDecoration, + activeBorderGradient: activeBorderGradient ?? this.activeBorderGradient, + idleBorderGradient: idleBorderGradient ?? this.idleBorderGradient, + borderRadius: borderRadius ?? this.borderRadius, + ); + + /// Merges [this] [MessageInputTheme] with the [other] + MessageInputTheme merge(MessageInputTheme? other) { + if (other == null) return this; + return copyWith( + sendAnimationDuration: other.sendAnimationDuration, + inputBackground: other.inputBackground, + actionButtonColor: other.actionButtonColor, + actionButtonIdleColor: other.actionButtonIdleColor, + sendButtonColor: other.sendButtonColor, + sendButtonIdleColor: other.sendButtonIdleColor, + inputTextStyle: + inputTextStyle?.merge(other.inputTextStyle) ?? other.inputTextStyle, + inputDecoration: inputDecoration?.merge(other.inputDecoration) ?? + other.inputDecoration, + activeBorderGradient: other.activeBorderGradient, + idleBorderGradient: other.idleBorderGradient, + borderRadius: other.borderRadius, + expandButtonColor: other.expandButtonColor, + ); + } +} \ No newline at end of file diff --git a/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart new file mode 100644 index 00000000..49bd05f9 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart @@ -0,0 +1,111 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; + +/// Overrides the default style of [MessageListView] descendants. +/// +/// See also: +/// +/// * [MessageListViewThemeData], which is used to configure this theme. +class MessageListViewTheme extends InheritedTheme { + /// Creates a [MessageListViewTheme]. + /// + /// The [data] parameter must not be null. + const MessageListViewTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final MessageListViewThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [MessageListViewTheme] widget, then + /// [StreamChatThemeData.messageListViewTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// MessageListViewTheme theme = MessageListViewTheme.of(context); + /// ``` + static MessageListViewThemeData of(BuildContext context) { + final messageListViewTheme = + context.dependOnInheritedWidgetOfExactType(); + return messageListViewTheme?.data ?? + StreamChatTheme.of(context).messageListViewTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) => + MessageListViewTheme(data: data, child: child); + + @override + bool updateShouldNotify(MessageListViewTheme oldWidget) => + data != oldWidget.data; +} + +/// A style that overrides the default appearance of [MessageListView]s when +/// used with [MessageListViewTheme] or with the overall [StreamChatTheme]'s +/// [StreamChatThemeData.messageListViewTheme]. +/// +/// See also: +/// +/// * [MessageListViewTheme], the theme which is configured with this class. +/// * [StreamChatThemeData.messageListViewTheme], which can be used to override +/// the default style for [MessageListView]s below the overall +/// [StreamChatTheme]. +class MessageListViewThemeData with Diagnosticable { + /// Creates a [MessageListViewThemeData]. + const MessageListViewThemeData({ + this.backgroundColor, + }); + + /// The color of the [MessageListView] background. + final Color? backgroundColor; + + /// Copies this [MessageListViewThemeData] to another. + MessageListViewThemeData copyWith({ + Color? backgroundColor, + }) => + MessageListViewThemeData( + backgroundColor: backgroundColor ?? this.backgroundColor, + ); + + /// Linearly interpolate between two [MessageListView] themes. + /// + /// All the properties must be non-null. + MessageListViewThemeData lerp( + MessageListViewThemeData a, + MessageListViewThemeData b, + double t, + ) => + MessageListViewThemeData( + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + ); + + /// Merges one [MessageListViewThemeData] with another. + MessageListViewThemeData merge(MessageListViewThemeData? other) { + if (other == null) return this; + return copyWith( + backgroundColor: other.backgroundColor, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MessageListViewThemeData && + runtimeType == other.runtimeType && + backgroundColor == other.backgroundColor; + + @override + int get hashCode => backgroundColor.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ColorProperty('backgroundColor', backgroundColor)); + } +} diff --git a/packages/stream_chat_flutter/lib/src/theme/message_search_list_view_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_search_list_view_theme.dart new file mode 100644 index 00000000..670e05e7 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/message_search_list_view_theme.dart @@ -0,0 +1,112 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; + +/// Overrides the default style of [MessageSearchListView] descendants. +/// +/// See also: +/// +/// * [UserListViewThemeData], which is used to configure this theme. +class MessageSearchListViewTheme extends InheritedTheme { + /// Creates a [UserListViewTheme]. + /// + /// The [data] parameter must not be null. + const MessageSearchListViewTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final MessageSearchListViewThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [MessageSearchListView] widget, then + /// [StreamChatThemeData.messageSearchListViewTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// MessageSearchListViewTheme theme = MessageSearchListViewTheme.of(context); + /// ``` + static MessageSearchListViewThemeData of(BuildContext context) { + final messageSearchListViewTheme = context + .dependOnInheritedWidgetOfExactType(); + return messageSearchListViewTheme?.data ?? + StreamChatTheme.of(context).messageSearchListViewTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) => + MessageSearchListViewTheme(data: data, child: child); + + @override + bool updateShouldNotify(MessageSearchListViewTheme oldWidget) => + data != oldWidget.data; +} + +/// A style that overrides the default appearance of [MessageSearchListView]s +/// when used with [MessageSearchListView] or with the overall +/// [StreamChatTheme]'s [StreamChatThemeData.messageSearchListViewTheme]. +/// +/// See also: +/// +/// * [MessageSearchListViewTheme], the theme which is configured with this +/// class. +/// * [StreamChatThemeData.messageSearchListViewTheme], which can be used to +/// override the default style for [UserListView]s below the overall +/// [StreamChatTheme]. +class MessageSearchListViewThemeData with Diagnosticable { + /// Creates a [MessageSearchListViewThemeData]. + const MessageSearchListViewThemeData({ + this.backgroundColor, + }); + + /// The color of the [MessageSearchListView] background. + final Color? backgroundColor; + + /// Copies this [MessageSearchListViewThemeData] to another. + MessageSearchListViewThemeData copyWith({ + Color? backgroundColor, + }) => + MessageSearchListViewThemeData( + backgroundColor: backgroundColor ?? this.backgroundColor, + ); + + /// Linearly interpolate between two [UserListViewThemeData] themes. + /// + /// All the properties must be non-null. + MessageSearchListViewThemeData lerp( + MessageSearchListViewThemeData a, + MessageSearchListViewThemeData b, + double t, + ) => + MessageSearchListViewThemeData( + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + ); + + /// Merges one [MessageSearchListViewThemeData] with another. + MessageSearchListViewThemeData merge(MessageSearchListViewThemeData? other) { + if (other == null) return this; + return copyWith( + backgroundColor: other.backgroundColor, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MessageSearchListViewThemeData && + runtimeType == other.runtimeType && + backgroundColor == other.backgroundColor; + + @override + int get hashCode => backgroundColor.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ColorProperty('backgroundColor', backgroundColor)); + } +} diff --git a/packages/stream_chat_flutter/lib/src/theme/message_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_theme.dart new file mode 100644 index 00000000..87543cb1 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/message_theme.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; + +/// Class for getting message theme +class MessageTheme { + /// Constructor into [MessageTheme] + const MessageTheme({ + this.replies, + this.messageText, + this.messageAuthor, + this.messageLinks, + this.messageBackgroundColor, + this.messageBorderColor, + this.reactionsBackgroundColor, + this.reactionsBorderColor, + this.reactionsMaskColor, + this.avatarTheme, + this.createdAt, + }); + + /// Text style for message text + final TextStyle? messageText; + + /// Text style for message author + final TextStyle? messageAuthor; + + /// Text style for message links + final TextStyle? messageLinks; + + /// Text style for created at text + final TextStyle? createdAt; + + /// Text style for replies + final TextStyle? replies; + + /// Color for messageBackgroundColor + final Color? messageBackgroundColor; + + /// Color for message border color + final Color? messageBorderColor; + + /// Color for reactions + final Color? reactionsBackgroundColor; + + /// Colors reaction border + final Color? reactionsBorderColor; + + /// Color for reaction mask + final Color? reactionsMaskColor; + + /// Theme of the avatar + final AvatarTheme? avatarTheme; + + /// Copy with a theme + MessageTheme copyWith({ + TextStyle? messageText, + TextStyle? messageAuthor, + TextStyle? messageLinks, + TextStyle? createdAt, + TextStyle? replies, + Color? messageBackgroundColor, + Color? messageBorderColor, + AvatarTheme? avatarTheme, + Color? reactionsBackgroundColor, + Color? reactionsBorderColor, + Color? reactionsMaskColor, + }) => + 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, + reactionsMaskColor: reactionsMaskColor ?? this.reactionsMaskColor, + ); + + /// Merge with a theme + 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, + reactionsMaskColor: other.reactionsMaskColor, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/theme/text_theme.dart b/packages/stream_chat_flutter/lib/src/theme/text_theme.dart new file mode 100644 index 00000000..ec70cf83 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/text_theme.dart @@ -0,0 +1,163 @@ +import 'package:flutter/material.dart'; + +/// Class for holding text theme +class TextTheme { + /// Initialise light text theme + 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, + ), + }); + + /// Initialise with dark theme + 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, + ), + }); + + /// Text theme for title + final TextStyle title; + + /// Body Text theme for headline + final TextStyle headlineBold; + + /// Text theme for headline + final TextStyle headline; + + /// Bold Text theme for body + final TextStyle bodyBold; + + /// Text theme body + final TextStyle body; + + /// Bold Text theme for footnote + final TextStyle footnoteBold; + + /// Text theme for footnote + final TextStyle footnote; + + /// Bold Text theme for caption + final TextStyle captionBold; + + /// Copy with theme + TextTheme copyWith({ + Brightness brightness = Brightness.light, + TextStyle? body, + TextStyle? title, + TextStyle? headlineBold, + TextStyle? headline, + TextStyle? bodyBold, + TextStyle? footnoteBold, + TextStyle? footnote, + TextStyle? captionBold, + }) => + brightness == Brightness.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, + ); + + /// Merge text theme + TextTheme merge(TextTheme? other) { + if (other == null) return this; + return copyWith( + body: body.merge(other.body), + title: title.merge(other.title), + headlineBold: headlineBold.merge(other.headlineBold), + headline: headline.merge(other.headline), + bodyBold: bodyBold.merge(other.bodyBold), + footnoteBold: footnoteBold.merge(other.footnoteBold), + footnote: footnote.merge(other.footnote), + captionBold: captionBold.merge(other.captionBold), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/theme/themes.dart b/packages/stream_chat_flutter/lib/src/theme/themes.dart new file mode 100644 index 00000000..4236f874 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/themes.dart @@ -0,0 +1,15 @@ +export 'avatar_theme.dart'; +export 'channel_header_theme.dart'; +export 'channel_list_header_theme.dart'; +export 'channel_list_view_theme.dart'; +export 'channel_preview_theme.dart'; +export 'channel_theme.dart'; +export 'color_theme.dart'; +export 'gallery_footer_theme.dart'; +export 'gallery_header_theme.dart'; +export 'message_input_theme.dart'; +export 'message_list_view_theme.dart'; +export 'message_search_list_view_theme.dart'; +export 'message_theme.dart'; +export 'text_theme.dart'; +export 'user_list_view_theme.dart'; \ No newline at end of file diff --git a/packages/stream_chat_flutter/lib/src/theme/user_list_view_theme.dart b/packages/stream_chat_flutter/lib/src/theme/user_list_view_theme.dart new file mode 100644 index 00000000..0d99ccbd --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/theme/user_list_view_theme.dart @@ -0,0 +1,111 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; + +/// Overrides the default style of [UserListView] descendants. +/// +/// See also: +/// +/// * [UserListViewThemeData], which is used to configure this theme. +class UserListViewTheme extends InheritedTheme { + /// Creates a [UserListViewTheme]. + /// + /// The [data] parameter must not be null. + const UserListViewTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final UserListViewThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [UserListViewTheme] widget, then + /// [StreamChatThemeData.userListViewTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// UserListViewTheme theme = UserListViewTheme.of(context); + /// ``` + static UserListViewThemeData of(BuildContext context) { + final userListViewTheme = + context.dependOnInheritedWidgetOfExactType(); + return userListViewTheme?.data ?? + StreamChatTheme.of(context).userListViewTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) => + UserListViewTheme(data: data, child: child); + + @override + bool updateShouldNotify(UserListViewTheme oldWidget) => + data != oldWidget.data; +} + +/// A style that overrides the default appearance of [UserListView]s when +/// used with [UserListViewTheme] or with the overall [StreamChatTheme]'s +/// [StreamChatThemeData.userListViewTheme]. +/// +/// See also: +/// +/// * [UserListViewTheme], the theme which is configured with this class. +/// * [StreamChatThemeData.userListViewTheme], which can be used to override +/// the default style for [UserListView]s below the overall +/// [StreamChatTheme]. +class UserListViewThemeData with Diagnosticable { + /// Creates a [UserListViewThemeData]. + const UserListViewThemeData({ + this.backgroundColor, + }); + + /// The color of the [ChannelListView] background. + final Color? backgroundColor; + + /// Copies this [ChannelListViewThemeData] to another. + UserListViewThemeData copyWith({ + Color? backgroundColor, + }) => + UserListViewThemeData( + backgroundColor: backgroundColor ?? this.backgroundColor, + ); + + /// Linearly interpolate between two [UserListViewThemeData] themes. + /// + /// All the properties must be non-null. + UserListViewThemeData lerp( + UserListViewThemeData a, + UserListViewThemeData b, + double t, + ) => + UserListViewThemeData( + backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), + ); + + /// Merges one [UserListViewThemeData] with another. + UserListViewThemeData merge(UserListViewThemeData? other) { + if (other == null) return this; + return copyWith( + backgroundColor: other.backgroundColor, + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is UserListViewThemeData && + runtimeType == other.runtimeType && + backgroundColor == other.backgroundColor; + + @override + int get hashCode => backgroundColor.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ColorProperty('backgroundColor', backgroundColor)); + } +} diff --git a/packages/stream_chat_flutter/lib/src/user_list_view.dart b/packages/stream_chat_flutter/lib/src/user_list_view.dart index 39d4453d..0374ffc8 100644 --- a/packages/stream_chat_flutter/lib/src/user_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/user_list_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 12049fc6..0a65d672 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -33,6 +33,7 @@ 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/theme/themes.dart'; export 'src/thread_header.dart'; export 'src/typing_indicator.dart'; export 'src/unread_indicator.dart'; From f90fd713c20844341cc086eb78056f7606286c7a Mon Sep 17 00:00:00 2001 From: groovinchip Date: Tue, 27 Jul 2021 10:32:49 -0400 Subject: [PATCH 002/121] chore: upgrade AvatarTheme * `AvatarTheme` is now an `InheritedTheme` and has a corresponding `AvatarThemeData` class --- .../example/lib/tutorial_part_6.dart | 4 +- .../lib/src/stream_chat_theme.dart | 23 +++- .../lib/src/theme/avatar_theme.dart | 113 +++++++++++++++--- .../lib/src/theme/channel_header_theme.dart | 4 +- .../src/theme/channel_list_header_theme.dart | 4 +- .../lib/src/theme/channel_preview_theme.dart | 4 +- .../lib/src/theme/gallery_header_theme.dart | 2 +- .../lib/src/theme/message_theme.dart | 4 +- 8 files changed, 127 insertions(+), 31 deletions(-) diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart index cf94b568..1cf34d27 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart @@ -59,7 +59,7 @@ class MyApp extends StatelessWidget { final colorTheme = defaultTheme.colorTheme; final customTheme = defaultTheme.merge(StreamChatThemeData( channelPreviewTheme: ChannelPreviewTheme( - avatarTheme: AvatarTheme( + avatarTheme: AvatarThemeData( borderRadius: BorderRadius.circular(8), ), ), @@ -68,7 +68,7 @@ class MyApp extends StatelessWidget { messageText: TextStyle( color: colorTheme.barsBg, ), - avatarTheme: AvatarTheme( + avatarTheme: AvatarThemeData( borderRadius: BorderRadius.circular(8), ), ), diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index 4336325b..d2eb0df0 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -62,6 +62,7 @@ class StreamChatThemeData { ChannelListViewThemeData? channelListViewTheme, UserListViewThemeData? userListViewTheme, MessageSearchListViewThemeData? messageSearchListViewTheme, + AvatarThemeData? avatarTheme, }) { brightness ??= colorTheme?.brightness ?? Brightness.light; final isDark = brightness == Brightness.dark; @@ -89,6 +90,7 @@ class StreamChatThemeData { channelListViewTheme: channelListViewTheme, userListViewTheme: userListViewTheme, messageSearchListViewTheme: messageSearchListViewTheme, + avatarTheme: avatarTheme, ); return defaultData.merge(customizedData); @@ -121,6 +123,7 @@ class StreamChatThemeData { required this.channelListViewTheme, required this.userListViewTheme, required this.messageSearchListViewTheme, + required this.avatarTheme, }); /// Create a theme from a Material [Theme] @@ -188,6 +191,9 @@ class StreamChatThemeData { /// Theme configuration for the [] widget. final MessageSearchListViewThemeData messageSearchListViewTheme; + /// Theme configuration for the [] widget. + final AvatarThemeData avatarTheme; + /// Creates a copy of [StreamChatThemeData] with specified attributes /// overridden. StreamChatThemeData copyWith({ @@ -208,6 +214,7 @@ class StreamChatThemeData { ChannelListViewThemeData? channelListViewTheme, UserListViewThemeData? userListViewTheme, MessageSearchListViewThemeData? messageSearchListViewTheme, + AvatarThemeData? avatarTheme, }) => StreamChatThemeData.raw( channelListHeaderTheme: @@ -230,6 +237,7 @@ class StreamChatThemeData { userListViewTheme: userListViewTheme ?? this.userListViewTheme, messageSearchListViewTheme: messageSearchListViewTheme ?? this.messageSearchListViewTheme, + avatarTheme: avatarTheme ?? this.avatarTheme, ); /// Merge themes @@ -257,6 +265,7 @@ class StreamChatThemeData { userListViewTheme: userListViewTheme.merge(other.userListViewTheme), messageSearchListViewTheme: messageSearchListViewTheme.merge(other.messageSearchListViewTheme), + avatarTheme: avatarTheme.merge(other.avatarTheme), ); } @@ -271,7 +280,7 @@ class StreamChatThemeData { IconThemeData(color: colorTheme.textHighEmphasis.withOpacity(.5)); final channelTheme = ChannelTheme( channelHeaderTheme: ChannelHeaderTheme( - avatarTheme: AvatarTheme( + avatarTheme: AvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, @@ -287,7 +296,7 @@ class StreamChatThemeData { ); final channelPreviewTheme = ChannelPreviewTheme( unreadCounterColor: colorTheme.accentError, - avatarTheme: AvatarTheme( + avatarTheme: AvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, @@ -315,7 +324,7 @@ class StreamChatThemeData { ), channelPreviewTheme: channelPreviewTheme, channelListHeaderTheme: ChannelListHeaderTheme( - avatarTheme: AvatarTheme( + avatarTheme: AvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, @@ -338,7 +347,7 @@ class StreamChatThemeData { reactionsBorderColor: colorTheme.borders, reactionsMaskColor: colorTheme.appBg, messageBorderColor: colorTheme.disabled, - avatarTheme: AvatarTheme( + avatarTheme: AvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 32, @@ -364,7 +373,7 @@ class StreamChatThemeData { ), messageBackgroundColor: colorTheme.barsBg, messageBorderColor: colorTheme.borders, - avatarTheme: AvatarTheme( + avatarTheme: AvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 32, @@ -487,7 +496,9 @@ class StreamChatThemeData { messageSearchListViewTheme: MessageSearchListViewThemeData( backgroundColor: colorTheme.appBg, ), + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(8), + ), ); } } - diff --git a/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart b/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart index f3e68597..873bd259 100644 --- a/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart @@ -1,9 +1,62 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; -/// Theme for avatar -class AvatarTheme { - /// Constructor for creating [AvatarTheme] - AvatarTheme({ +/// Overrides the default style of [UserAvatar] descendants. +/// +/// See also: +/// +/// * [AvatarThemeData], which is used to configure this theme. +class AvatarTheme extends InheritedTheme { + /// Creates an [AvatarTheme]. + /// + /// The [data] parameter must not be null. + const AvatarTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final AvatarThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [GalleryHeaderTheme] widget, then + /// [StreamChatThemeData.avatarTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// final theme = AvatarTheme.of(context); + /// ``` + static AvatarThemeData of(BuildContext context) { + final avatarTheme = + context.dependOnInheritedWidgetOfExactType(); + return avatarTheme?.data ?? StreamChatTheme.of(context).avatarTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) => + AvatarTheme(data: data, child: child); + + @override + bool updateShouldNotify(AvatarTheme oldWidget) => data != oldWidget.data; +} + +/// A style that overrides the default appearance of [UserAvatar]s when used +/// with [AvatarTheme] or with the overall [StreamChatTheme]'s +/// [StreamChatThemeData.avatarTheme]. +/// +/// See also: +/// +/// * [AvatarTheme], the theme which is configured with this class. +/// * [StreamChatThemeData.avatarTheme], which can be used to override +/// the default style for [UserAvatar]s below the overall [StreamChatTheme]. +class AvatarThemeData with Diagnosticable { + /// Creates an [AvatarThemeData]. + AvatarThemeData({ BoxConstraints? constraints, BorderRadius? borderRadius, }) : _constraints = constraints, @@ -15,30 +68,62 @@ class AvatarTheme { /// Get constraints for avatar BoxConstraints get constraints => _constraints ?? - const BoxConstraints.tightFor( - height: 32, - width: 32, - ); + const BoxConstraints.tightFor( + height: 32, + width: 32, + ); /// Get border radius BorderRadius get borderRadius => _borderRadius ?? BorderRadius.circular(20); - /// Copy with another theme - AvatarTheme copyWith({ + /// Copy this [AvatarThemeData] to another. + AvatarThemeData copyWith({ BoxConstraints? constraints, BorderRadius? borderRadius, }) => - AvatarTheme( + AvatarThemeData( constraints: constraints ?? _constraints, borderRadius: borderRadius ?? _borderRadius, ); - /// Merge with another AvatarTheme - AvatarTheme merge(AvatarTheme? other) { + /// Linearly interpolate between two [UserAvatar] themes. + /// + /// All the properties must be non-null. + AvatarThemeData lerp( + AvatarThemeData a, + AvatarThemeData b, + double t, + ) => + AvatarThemeData( + borderRadius: BorderRadius.lerp(a.borderRadius, b.borderRadius, t), + constraints: BoxConstraints.lerp(a.constraints, b.constraints, t), + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AvatarThemeData && + runtimeType == other.runtimeType && + _constraints == other._constraints && + _borderRadius == other._borderRadius; + + @override + int get hashCode => _constraints.hashCode ^ _borderRadius.hashCode; + + /// Merges one [AvatarThemeData] with the another + AvatarThemeData merge(AvatarThemeData? other) { if (other == null) return this; return copyWith( constraints: other._constraints, borderRadius: other._borderRadius, ); } -} \ No newline at end of file + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('borderRadius', borderRadius)) + ..add(DiagnosticsProperty('constraints', constraints)); + } +} diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart index 2072b3c5..be1d0347 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart @@ -18,7 +18,7 @@ class ChannelHeaderTheme { final TextStyle? subtitle; /// Theme for avatar - final AvatarTheme? avatarTheme; + final AvatarThemeData? avatarTheme; /// Color for [ChannelHeaderTheme] final Color? color; @@ -27,7 +27,7 @@ class ChannelHeaderTheme { ChannelHeaderTheme copyWith({ TextStyle? title, TextStyle? subtitle, - AvatarTheme? avatarTheme, + AvatarThemeData? avatarTheme, Color? color, }) => ChannelHeaderTheme( diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart index ca36b9bd..8b5984bd 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart @@ -14,7 +14,7 @@ class ChannelListHeaderTheme { final TextStyle? title; /// Theme dedicated to the userAvatar - final AvatarTheme? avatarTheme; + final AvatarThemeData? avatarTheme; /// Background color of the appbar final Color? color; @@ -22,7 +22,7 @@ class ChannelListHeaderTheme { /// Returns a new [ChannelListHeaderTheme] replacing some of its properties ChannelListHeaderTheme copyWith({ TextStyle? title, - AvatarTheme? avatarTheme, + AvatarThemeData? avatarTheme, Color? color, }) => ChannelListHeaderTheme( diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart index 4643bb8d..61975b3f 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart @@ -23,7 +23,7 @@ class ChannelPreviewTheme { final TextStyle? lastMessageAt; /// Avatar theme - final AvatarTheme? avatarTheme; + final AvatarThemeData? avatarTheme; /// Unread counter color final Color? unreadCounterColor; @@ -36,7 +36,7 @@ class ChannelPreviewTheme { TextStyle? title, TextStyle? subtitle, TextStyle? lastMessageAt, - AvatarTheme? avatarTheme, + AvatarThemeData? avatarTheme, Color? unreadCounterColor, double? indicatorIconSize, }) => diff --git a/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart index 9b5faa83..611a77bc 100644 --- a/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart @@ -8,7 +8,7 @@ import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; /// /// * [GalleryHeaderThemeData], which is used to configure this theme. class GalleryHeaderTheme extends InheritedTheme { - /// Creates an [GalleryHeaderTheme]. + /// Creates a [GalleryHeaderTheme]. /// /// The [data] parameter must not be null. const GalleryHeaderTheme({ diff --git a/packages/stream_chat_flutter/lib/src/theme/message_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_theme.dart index 87543cb1..e9deb1f4 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_theme.dart @@ -49,7 +49,7 @@ class MessageTheme { final Color? reactionsMaskColor; /// Theme of the avatar - final AvatarTheme? avatarTheme; + final AvatarThemeData? avatarTheme; /// Copy with a theme MessageTheme copyWith({ @@ -60,7 +60,7 @@ class MessageTheme { TextStyle? replies, Color? messageBackgroundColor, Color? messageBorderColor, - AvatarTheme? avatarTheme, + AvatarThemeData? avatarTheme, Color? reactionsBackgroundColor, Color? reactionsBorderColor, Color? reactionsMaskColor, From 66ea5557e88ace85dbaccbfe3add1320803e031f Mon Sep 17 00:00:00 2001 From: groovinchip Date: Tue, 27 Jul 2021 10:47:01 -0400 Subject: [PATCH 003/121] test: create tests for AvatarThemeData --- .../lib/src/theme/avatar_theme.dart | 2 +- .../test/src/avatar_theme_test.dart | 50 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 packages/stream_chat_flutter/test/src/avatar_theme_test.dart diff --git a/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart b/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart index 873bd259..7af1b976 100644 --- a/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart @@ -56,7 +56,7 @@ class AvatarTheme extends InheritedTheme { /// the default style for [UserAvatar]s below the overall [StreamChatTheme]. class AvatarThemeData with Diagnosticable { /// Creates an [AvatarThemeData]. - AvatarThemeData({ + const AvatarThemeData({ BoxConstraints? constraints, BorderRadius? borderRadius, }) : _constraints = constraints, diff --git a/packages/stream_chat_flutter/test/src/avatar_theme_test.dart b/packages/stream_chat_flutter/test/src/avatar_theme_test.dart new file mode 100644 index 00000000..63e6da86 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/avatar_theme_test.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + test('AvatarThemeData copyWith, ==, hashCode basics', () { + expect(const AvatarThemeData(), const AvatarThemeData().copyWith()); + expect(const AvatarThemeData().hashCode, + const AvatarThemeData().copyWith().hashCode); + }); + + group('AvatarThemeData lerps correctly', () { + test('Lerp completely', () { + expect( + const AvatarThemeData() + .lerp(_avatarThemeDataControl1, _avatarThemeDataControl2, 1), + _avatarThemeDataControl2); + }); + + test('Lerp halfway', () { + expect( + const AvatarThemeData() + .lerp(_avatarThemeDataControl1, _avatarThemeDataControl2, 0.5), + _avatarThemeDataControlMidLerp); + }); + }); + + test('Merging two AvatarThemeData results in the latter', () { + expect(_avatarThemeDataControl1.merge(_avatarThemeDataControl2), + _avatarThemeDataControl2); + }); +} + +const _avatarThemeDataControl1 = AvatarThemeData(); + +final _avatarThemeDataControlMidLerp = AvatarThemeData( + borderRadius: BorderRadius.circular(16), + constraints: const BoxConstraints.tightFor( + height: 33, + width: 33, + ), +); + +final _avatarThemeDataControl2 = AvatarThemeData( + borderRadius: BorderRadius.circular(12), + constraints: const BoxConstraints.tightFor( + height: 34, + width: 34, + ), +); From 1fa8c5264ab18d00e42d213deab85a6078bdef84 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Tue, 27 Jul 2021 11:46:19 -0400 Subject: [PATCH 004/121] chore: upgrade ChannelHeaderTheme --- .../lib/src/stream_chat_theme.dart | 8 +-- .../lib/src/theme/channel_header_theme.dart | 72 ++++++++++++++++--- .../lib/src/theme/channel_theme.dart | 4 +- 3 files changed, 69 insertions(+), 15 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index d2eb0df0..38511e38 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -188,10 +188,10 @@ class StreamChatThemeData { /// Theme configuration for the [UserListView] widget. final UserListViewThemeData userListViewTheme; - /// Theme configuration for the [] widget. + /// Theme configuration for the [MessageSearchListView] widget. final MessageSearchListViewThemeData messageSearchListViewTheme; - /// Theme configuration for the [] widget. + /// Theme configuration for the [UserAvatar] widget. final AvatarThemeData avatarTheme; /// Creates a copy of [StreamChatThemeData] with specified attributes @@ -279,7 +279,7 @@ class StreamChatThemeData { final iconTheme = IconThemeData(color: colorTheme.textHighEmphasis.withOpacity(.5)); final channelTheme = ChannelTheme( - channelHeaderTheme: ChannelHeaderTheme( + channelHeaderTheme: ChannelHeaderThemeData( avatarTheme: AvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( @@ -497,7 +497,7 @@ class StreamChatThemeData { backgroundColor: colorTheme.appBg, ), avatarTheme: AvatarThemeData( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(20), ), ); } diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart index be1d0347..8ea92615 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart @@ -1,10 +1,64 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; -/// Theme for [ChannelHeader] -class ChannelHeaderTheme { - /// Constructor for creating a [ChannelHeaderTheme] +/// Overrides the default style of [ChannelHeader] descendants. +/// +/// See also: +/// +/// * [ChannelHeaderThemeData], which is used to configure this theme. +class ChannelHeaderTheme extends InheritedTheme { + /// Creates a [ChannelHeaderTheme]. + /// + /// The [data] parameter must not be null. const ChannelHeaderTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final ChannelHeaderThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [ChannelHeaderTheme] widget, then + /// [StreamChatThemeData.channelTheme.channelHeaderTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// final theme = ChannelHeaderTheme.of(context); + /// ``` + static ChannelHeaderThemeData of(BuildContext context) { + final channelHeaderTheme = + context.dependOnInheritedWidgetOfExactType(); + return channelHeaderTheme?.data ?? + StreamChatTheme.of(context).channelTheme.channelHeaderTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) => + ChannelHeaderTheme(data: data, child: child); + + @override + bool updateShouldNotify(ChannelHeaderTheme oldWidget) => + data != oldWidget.data; +} + +/// A style that overrides the default appearance of [ChannelHeader]s when used +/// with [ChannelHeaderTheme] or with the overall [StreamChatTheme]'s +/// [StreamChatThemeData.channelHeaderTheme]. +/// +/// See also: +/// +/// * [ChannelHeaderTheme], the theme which is configured with this class. +/// * [StreamChatThemeData.channelHeaderTheme], which can be used to override +/// the default style for [ChannelHeader]s below the overall [StreamChatTheme]. +class ChannelHeaderThemeData with Diagnosticable { + /// Creates a [ChannelHeaderThemeData] + const ChannelHeaderThemeData({ this.title, this.subtitle, this.avatarTheme, @@ -20,25 +74,25 @@ class ChannelHeaderTheme { /// Theme for avatar final AvatarThemeData? avatarTheme; - /// Color for [ChannelHeaderTheme] + /// Color for [ChannelHeaderThemeData] final Color? color; /// Copy with theme - ChannelHeaderTheme copyWith({ + ChannelHeaderThemeData copyWith({ TextStyle? title, TextStyle? subtitle, AvatarThemeData? avatarTheme, Color? color, }) => - ChannelHeaderTheme( + ChannelHeaderThemeData( title: title ?? this.title, subtitle: subtitle ?? this.subtitle, avatarTheme: avatarTheme ?? this.avatarTheme, color: color ?? this.color, ); - /// Merge with other [ChannelHeaderTheme] - ChannelHeaderTheme merge(ChannelHeaderTheme? other) { + /// Merge with other [ChannelHeaderThemeData] + ChannelHeaderThemeData merge(ChannelHeaderThemeData? other) { if (other == null) return this; return copyWith( title: title?.merge(other.title) ?? other.title, @@ -47,4 +101,4 @@ class ChannelHeaderTheme { color: other.color, ); } -} \ No newline at end of file +} diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_theme.dart index bf752f94..a8c6260d 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_theme.dart @@ -8,11 +8,11 @@ class ChannelTheme { }); /// Theme of the [ChannelHeader] widget - final ChannelHeaderTheme channelHeaderTheme; + final ChannelHeaderThemeData channelHeaderTheme; /// Creates a copy of [ChannelTheme] with specified attributes overridden. ChannelTheme copyWith({ - ChannelHeaderTheme? channelHeaderTheme, + ChannelHeaderThemeData? channelHeaderTheme, }) => ChannelTheme( channelHeaderTheme: channelHeaderTheme ?? this.channelHeaderTheme, From 8bf45f85019c87ed70947d4f1ae7378cc4da3bb6 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Tue, 27 Jul 2021 13:21:20 -0400 Subject: [PATCH 005/121] fix: remove duplicate exports in stream_chat_flutter.dart --- packages/stream_chat_flutter/lib/stream_chat_flutter.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 0a65d672..c5684bbb 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -39,7 +39,5 @@ export 'src/typing_indicator.dart'; export 'src/unread_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/utils.dart'; From 3a072fc836919b096539a9bdca71296a0658ede9 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Tue, 27 Jul 2021 13:33:58 -0400 Subject: [PATCH 006/121] chore: more upgrades for channel_header_theme.dart * Renamed: * title -> titleStyle * subtitle -> subtitleStyle * Added * == * hashcode * debugFillProperties * lerp --- .../lib/src/channel_header.dart | 4 +- .../lib/src/channel_info.dart | 2 +- .../lib/src/message_list_view.dart | 2 +- .../lib/src/stream_chat_theme.dart | 4 +- .../lib/src/theme/channel_header_theme.dart | 65 ++++++++++++++++--- .../lib/src/theme/gallery_header_theme.dart | 2 +- .../lib/src/thread_header.dart | 8 +-- 7 files changed, 66 insertions(+), 21 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel_header.dart index 8f586516..4df44765 100644 --- a/packages/stream_chat_flutter/lib/src/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_header.dart @@ -169,7 +169,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { title ?? ChannelName( textStyle: chatThemeData - .channelTheme.channelHeaderTheme.title, + .channelTheme.channelHeaderTheme.titleStyle, ), const SizedBox(height: 2), subtitle ?? @@ -177,7 +177,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { showTypingIndicator: showTypingIndicator, channel: channel, textStyle: chatThemeData - .channelTheme.channelHeaderTheme.subtitle, + .channelTheme.channelHeaderTheme.subtitleStyle, ), ], ), diff --git a/packages/stream_chat_flutter/lib/src/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel_info.dart index 89894324..67e18801 100644 --- a/packages/stream_chat_flutter/lib/src/channel_info.dart +++ b/packages/stream_chat_flutter/lib/src/channel_info.dart @@ -64,7 +64,7 @@ class ChannelInfo extends StatelessWidget { style: StreamChatTheme.of(context) .channelTheme .channelHeaderTheme - .subtitle, + .subtitleStyle, ); } else { final userId = StreamChat.of(context).currentUser?.id; diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index e7279579..303c76db 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -628,7 +628,7 @@ class _MessageListViewState extends State { child: Text( '$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}', textAlign: TextAlign.center, - style: _streamTheme.channelTheme.channelHeaderTheme.subtitle, + style: _streamTheme.channelTheme.channelHeaderTheme.subtitleStyle, ), ), ); diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index 38511e38..dad5dadb 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -288,8 +288,8 @@ class StreamChatThemeData { ), ), color: colorTheme.barsBg, - title: textTheme.headlineBold, - subtitle: textTheme.footnote.copyWith( + titleStyle: textTheme.headlineBold, + subtitleStyle: textTheme.footnote.copyWith( color: const Color(0xff7A7A7A), ), ), diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart index 8ea92615..2d3253dc 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; /// Overrides the default style of [ChannelHeader] descendants. /// @@ -59,17 +60,17 @@ class ChannelHeaderTheme extends InheritedTheme { class ChannelHeaderThemeData with Diagnosticable { /// Creates a [ChannelHeaderThemeData] const ChannelHeaderThemeData({ - this.title, - this.subtitle, + this.titleStyle, + this.subtitleStyle, this.avatarTheme, this.color, }); /// Theme for title - final TextStyle? title; + final TextStyle? titleStyle; /// Theme for subtitle - final TextStyle? subtitle; + final TextStyle? subtitleStyle; /// Theme for avatar final AvatarThemeData? avatarTheme; @@ -79,26 +80,70 @@ class ChannelHeaderThemeData with Diagnosticable { /// Copy with theme ChannelHeaderThemeData copyWith({ - TextStyle? title, - TextStyle? subtitle, + TextStyle? titleStyle, + TextStyle? subtitleStyle, AvatarThemeData? avatarTheme, Color? color, }) => ChannelHeaderThemeData( - title: title ?? this.title, - subtitle: subtitle ?? this.subtitle, + titleStyle: titleStyle ?? this.titleStyle, + subtitleStyle: subtitleStyle ?? this.subtitleStyle, avatarTheme: avatarTheme ?? this.avatarTheme, color: color ?? this.color, ); + /// Linearly interpolate between two [ChannelHeaderThemeData]. + /// + /// All the properties must be non-null. + ChannelHeaderThemeData lerp( + ChannelHeaderThemeData a, + ChannelHeaderThemeData b, + double t, + ) => + ChannelHeaderThemeData( + titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t), + subtitleStyle: TextStyle.lerp(a.subtitleStyle, b.subtitleStyle, t), + avatarTheme: + const AvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), + color: Color.lerp(a.color, b.color, t), + ); + /// Merge with other [ChannelHeaderThemeData] ChannelHeaderThemeData merge(ChannelHeaderThemeData? other) { if (other == null) return this; return copyWith( - title: title?.merge(other.title) ?? other.title, - subtitle: subtitle?.merge(other.subtitle) ?? other.subtitle, + titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle, + subtitleStyle: + subtitleStyle?.merge(other.subtitleStyle) ?? other.subtitleStyle, avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme, color: other.color, ); } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ChannelHeaderThemeData && + runtimeType == other.runtimeType && + titleStyle == other.titleStyle && + subtitleStyle == other.subtitleStyle && + avatarTheme == other.avatarTheme && + color == other.color; + + @override + int get hashCode => + titleStyle.hashCode ^ + subtitleStyle.hashCode ^ + avatarTheme.hashCode ^ + color.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('title', titleStyle)) + ..add(DiagnosticsProperty('subtitle', subtitleStyle)) + ..add(DiagnosticsProperty('avatarTheme', avatarTheme)) + ..add(ColorProperty('color', color)); + } } diff --git a/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart index 611a77bc..862a9a9e 100644 --- a/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart @@ -86,7 +86,7 @@ class GalleryHeaderThemeData with Diagnosticable { /// The [TextStyle] to use for the [GalleryHeader] subtitle text. /// - /// Defaults to [ChannelPreviewTheme.subtitle]. + /// Defaults to [ChannelPreviewTheme.subtitleStyle]. final TextStyle? subtitleTextStyle; /// diff --git a/packages/stream_chat_flutter/lib/src/thread_header.dart b/packages/stream_chat_flutter/lib/src/thread_header.dart index bfde9a4c..0473ca48 100644 --- a/packages/stream_chat_flutter/lib/src/thread_header.dart +++ b/packages/stream_chat_flutter/lib/src/thread_header.dart @@ -112,12 +112,12 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { children: [ Text( 'with ', - style: chatThemeData.channelTheme.channelHeaderTheme.subtitle, + style: chatThemeData.channelTheme.channelHeaderTheme.subtitleStyle, ), Flexible( child: ChannelName( textStyle: - chatThemeData.channelTheme.channelHeaderTheme.subtitle, + chatThemeData.channelTheme.channelHeaderTheme.subtitleStyle, ), ), ], @@ -150,14 +150,14 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { title ?? Text( 'Thread Reply', - style: chatThemeData.channelTheme.channelHeaderTheme.title, + style: chatThemeData.channelTheme.channelHeaderTheme.titleStyle, ), const SizedBox(height: 2), if (showTypingIndicator) TypingIndicator( alignment: Alignment.center, channel: StreamChannel.of(context).channel, - style: chatThemeData.channelTheme.channelHeaderTheme.subtitle, + style: chatThemeData.channelTheme.channelHeaderTheme.subtitleStyle, parentId: parent.id, alternativeWidget: defaultSubtitle, ) From 362d55b89e783713d82256b3b9b2c93bb96359c4 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Tue, 27 Jul 2021 13:48:32 -0400 Subject: [PATCH 007/121] test: add tests for ChannelHeaderThemeData --- .../test/src/channel_header_theme_test.dart | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 packages/stream_chat_flutter/test/src/channel_header_theme_test.dart diff --git a/packages/stream_chat_flutter/test/src/channel_header_theme_test.dart b/packages/stream_chat_flutter/test/src/channel_header_theme_test.dart new file mode 100644 index 00000000..de9803b7 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/channel_header_theme_test.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart' hide TextTheme; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + test('ChannelHeaderThemeData copyWith, ==, hashCode basics', () { + expect(const ChannelHeaderThemeData(), + const ChannelHeaderThemeData().copyWith()); + expect(const ChannelHeaderThemeData().hashCode, + const ChannelHeaderThemeData().copyWith().hashCode); + }); + + group('ChannelHeaderThemeData lerps', () { + test( + '''Light ChannelHeaderThemeData lerps completely to dark ChannelHeaderThemeData''', + () { + expect( + const ChannelHeaderThemeData() + .lerp(_channelThemeControl, _channelThemeControlDark, 1), + _channelThemeControlDark); + }); + + test( + '''Light ChannelHeaderThemeData lerps halfway to dark ChannelHeaderThemeData''', + () { + expect( + const ChannelHeaderThemeData() + .lerp(_channelThemeControl, _channelThemeControlDark, 0.5), + _channelThemeControlDark); + }); + + test( + '''Dark ChannelHeaderThemeData lerps completely to light ChannelHeaderThemeData''', + () { + expect( + const ChannelHeaderThemeData() + .lerp(_channelThemeControlDark, _channelThemeControl, 1), + _channelThemeControl); + }); + }); + + test('Merging dark and light themes results in a dark theme', () { + expect(_channelThemeControl.merge(_channelThemeControlDark), + _channelThemeControlDark); + }); +} + +final _channelThemeControl = ChannelHeaderThemeData( + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + color: const Color(0xff101418), + titleStyle: TextTheme.light().headlineBold.copyWith( + color: const Color(0xffffffff), + ), + subtitleStyle: TextTheme.light().footnote.copyWith( + color: const Color(0xff7a7a7a), + ), +); + +final _channelThemeControlMidLerp = ChannelHeaderThemeData( + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + color: ColorTheme.light().barsBg, + titleStyle: TextTheme.light().headlineBold, + subtitleStyle: TextTheme.light().footnote.copyWith( + color: const Color(0xff7A7A7A), + ), +); + +final _channelThemeControlDark = ChannelHeaderThemeData( + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + color: ColorTheme.dark().barsBg, + titleStyle: TextTheme.dark().headlineBold, + subtitleStyle: TextTheme.dark().footnote.copyWith( + color: const Color(0xff7A7A7A), + ), +); From 82fa91609069c28ed2ac8d7b6cb507756c5d037e Mon Sep 17 00:00:00 2001 From: groovinchip Date: Tue, 27 Jul 2021 15:30:34 -0400 Subject: [PATCH 008/121] test: move theme test files into their own folder within test/src --- .../test/src/{ => theme}/avatar_theme_test.dart | 0 .../test/src/{ => theme}/channel_header_theme_test.dart | 0 .../test/src/{ => theme}/channel_list_view_theme_test.dart | 2 +- .../test/src/{ => theme}/gallery_footer_theme_test.dart | 0 .../test/src/{ => theme}/gallery_header_theme_test.dart | 0 .../test/src/{ => theme}/message_list_view_theme_test.dart | 2 +- .../src/{ => theme}/message_search_list_view_theme_test.dart | 2 +- .../test/src/{ => theme}/user_list_view_theme_test.dart | 2 +- 8 files changed, 4 insertions(+), 4 deletions(-) rename packages/stream_chat_flutter/test/src/{ => theme}/avatar_theme_test.dart (100%) rename packages/stream_chat_flutter/test/src/{ => theme}/channel_header_theme_test.dart (100%) rename packages/stream_chat_flutter/test/src/{ => theme}/channel_list_view_theme_test.dart (99%) rename packages/stream_chat_flutter/test/src/{ => theme}/gallery_footer_theme_test.dart (100%) rename packages/stream_chat_flutter/test/src/{ => theme}/gallery_header_theme_test.dart (100%) rename packages/stream_chat_flutter/test/src/{ => theme}/message_list_view_theme_test.dart (99%) rename packages/stream_chat_flutter/test/src/{ => theme}/message_search_list_view_theme_test.dart (99%) rename packages/stream_chat_flutter/test/src/{ => theme}/user_list_view_theme_test.dart (99%) diff --git a/packages/stream_chat_flutter/test/src/avatar_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/avatar_theme_test.dart similarity index 100% rename from packages/stream_chat_flutter/test/src/avatar_theme_test.dart rename to packages/stream_chat_flutter/test/src/theme/avatar_theme_test.dart diff --git a/packages/stream_chat_flutter/test/src/channel_header_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/channel_header_theme_test.dart similarity index 100% rename from packages/stream_chat_flutter/test/src/channel_header_theme_test.dart rename to packages/stream_chat_flutter/test/src/theme/channel_header_theme_test.dart diff --git a/packages/stream_chat_flutter/test/src/channel_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/channel_list_view_theme_test.dart similarity index 99% rename from packages/stream_chat_flutter/test/src/channel_list_view_theme_test.dart rename to packages/stream_chat_flutter/test/src/theme/channel_list_view_theme_test.dart index 80a21d25..38b1c524 100644 --- a/packages/stream_chat_flutter/test/src/channel_list_view_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/channel_list_view_theme_test.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { test('ChannelListViewThemeData copyWith, ==, hashCode basics', () { diff --git a/packages/stream_chat_flutter/test/src/gallery_footer_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart similarity index 100% rename from packages/stream_chat_flutter/test/src/gallery_footer_theme_test.dart rename to packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart diff --git a/packages/stream_chat_flutter/test/src/gallery_header_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart similarity index 100% rename from packages/stream_chat_flutter/test/src/gallery_header_theme_test.dart rename to packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart diff --git a/packages/stream_chat_flutter/test/src/message_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_list_view_theme_test.dart similarity index 99% rename from packages/stream_chat_flutter/test/src/message_list_view_theme_test.dart rename to packages/stream_chat_flutter/test/src/theme/message_list_view_theme_test.dart index ffe54dd0..476f5063 100644 --- a/packages/stream_chat_flutter/test/src/message_list_view_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_list_view_theme_test.dart @@ -3,7 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; class MockStreamChatClient extends Mock implements StreamChatClient {} diff --git a/packages/stream_chat_flutter/test/src/message_search_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart similarity index 99% rename from packages/stream_chat_flutter/test/src/message_search_list_view_theme_test.dart rename to packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart index e485e0d6..4da1c7dc 100644 --- a/packages/stream_chat_flutter/test/src/message_search_list_view_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { test('MessageSearchListViewThemeData copyWith, ==, hashCode basics', () { diff --git a/packages/stream_chat_flutter/test/src/user_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/user_list_view_theme_test.dart similarity index 99% rename from packages/stream_chat_flutter/test/src/user_list_view_theme_test.dart rename to packages/stream_chat_flutter/test/src/theme/user_list_view_theme_test.dart index e9b7985a..a22eebd0 100644 --- a/packages/stream_chat_flutter/test/src/user_list_view_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/user_list_view_theme_test.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'mocks.dart'; +import '../mocks.dart'; void main() { test('UserListViewThemeData copyWith, ==, hashCode basics', () { From de4e1e29e251d62c9802127760765436e10145d5 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Wed, 28 Jul 2021 12:49:14 -0400 Subject: [PATCH 009/121] chore: remove AvatarTheme --- .../lib/src/stream_chat_theme.dart | 12 ----- .../lib/src/theme/avatar_theme.dart | 54 +------------------ 2 files changed, 1 insertion(+), 65 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index dad5dadb..f83050a3 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -62,7 +62,6 @@ class StreamChatThemeData { ChannelListViewThemeData? channelListViewTheme, UserListViewThemeData? userListViewTheme, MessageSearchListViewThemeData? messageSearchListViewTheme, - AvatarThemeData? avatarTheme, }) { brightness ??= colorTheme?.brightness ?? Brightness.light; final isDark = brightness == Brightness.dark; @@ -90,7 +89,6 @@ class StreamChatThemeData { channelListViewTheme: channelListViewTheme, userListViewTheme: userListViewTheme, messageSearchListViewTheme: messageSearchListViewTheme, - avatarTheme: avatarTheme, ); return defaultData.merge(customizedData); @@ -123,7 +121,6 @@ class StreamChatThemeData { required this.channelListViewTheme, required this.userListViewTheme, required this.messageSearchListViewTheme, - required this.avatarTheme, }); /// Create a theme from a Material [Theme] @@ -191,9 +188,6 @@ class StreamChatThemeData { /// Theme configuration for the [MessageSearchListView] widget. final MessageSearchListViewThemeData messageSearchListViewTheme; - /// Theme configuration for the [UserAvatar] widget. - final AvatarThemeData avatarTheme; - /// Creates a copy of [StreamChatThemeData] with specified attributes /// overridden. StreamChatThemeData copyWith({ @@ -214,7 +208,6 @@ class StreamChatThemeData { ChannelListViewThemeData? channelListViewTheme, UserListViewThemeData? userListViewTheme, MessageSearchListViewThemeData? messageSearchListViewTheme, - AvatarThemeData? avatarTheme, }) => StreamChatThemeData.raw( channelListHeaderTheme: @@ -237,7 +230,6 @@ class StreamChatThemeData { userListViewTheme: userListViewTheme ?? this.userListViewTheme, messageSearchListViewTheme: messageSearchListViewTheme ?? this.messageSearchListViewTheme, - avatarTheme: avatarTheme ?? this.avatarTheme, ); /// Merge themes @@ -265,7 +257,6 @@ class StreamChatThemeData { userListViewTheme: userListViewTheme.merge(other.userListViewTheme), messageSearchListViewTheme: messageSearchListViewTheme.merge(other.messageSearchListViewTheme), - avatarTheme: avatarTheme.merge(other.avatarTheme), ); } @@ -496,9 +487,6 @@ class StreamChatThemeData { messageSearchListViewTheme: MessageSearchListViewThemeData( backgroundColor: colorTheme.appBg, ), - avatarTheme: AvatarThemeData( - borderRadius: BorderRadius.circular(20), - ), ); } } diff --git a/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart b/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart index 7af1b976..6c287638 100644 --- a/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart @@ -1,59 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; -import 'package:stream_chat_flutter/src/theme/themes.dart'; -/// Overrides the default style of [UserAvatar] descendants. -/// -/// See also: -/// -/// * [AvatarThemeData], which is used to configure this theme. -class AvatarTheme extends InheritedTheme { - /// Creates an [AvatarTheme]. - /// - /// The [data] parameter must not be null. - const AvatarTheme({ - Key? key, - required this.data, - required Widget child, - }) : super(key: key, child: child); - - /// The configuration of this theme. - final AvatarThemeData data; - - /// The closest instance of this class that encloses the given context. - /// - /// If there is no enclosing [GalleryHeaderTheme] widget, then - /// [StreamChatThemeData.avatarTheme] is used. - /// - /// Typical usage is as follows: - /// - /// ```dart - /// final theme = AvatarTheme.of(context); - /// ``` - static AvatarThemeData of(BuildContext context) { - final avatarTheme = - context.dependOnInheritedWidgetOfExactType(); - return avatarTheme?.data ?? StreamChatTheme.of(context).avatarTheme; - } - - @override - Widget wrap(BuildContext context, Widget child) => - AvatarTheme(data: data, child: child); - - @override - bool updateShouldNotify(AvatarTheme oldWidget) => data != oldWidget.data; -} - -/// A style that overrides the default appearance of [UserAvatar]s when used -/// with [AvatarTheme] or with the overall [StreamChatTheme]'s -/// [StreamChatThemeData.avatarTheme]. -/// -/// See also: -/// -/// * [AvatarTheme], the theme which is configured with this class. -/// * [StreamChatThemeData.avatarTheme], which can be used to override -/// the default style for [UserAvatar]s below the overall [StreamChatTheme]. +/// A style that overrides the default appearance of various avatar widgets. class AvatarThemeData with Diagnosticable { /// Creates an [AvatarThemeData]. const AvatarThemeData({ From cede1b572a5eda90ea084899c0c7d91ee9be37e3 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Wed, 28 Jul 2021 14:49:12 -0400 Subject: [PATCH 010/121] chore: run dartfmt on /theme --- .../src/theme/channel_list_header_theme.dart | 2 +- .../lib/src/theme/channel_preview_theme.dart | 4 +-- .../lib/src/theme/gallery_header_theme.dart | 34 +++++++++---------- .../lib/src/theme/message_input_theme.dart | 8 ++--- .../src/theme/message_list_view_theme.dart | 16 ++++----- .../lib/src/theme/themes.dart | 2 +- 6 files changed, 33 insertions(+), 33 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart index 8b5984bd..339361b9 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart @@ -40,4 +40,4 @@ class ChannelListHeaderTheme { color: other.color, ); } -} \ No newline at end of file +} diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart index 61975b3f..2fcecaf7 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart @@ -56,9 +56,9 @@ class ChannelPreviewTheme { title: title?.merge(other.title) ?? other.title, subtitle: subtitle?.merge(other.subtitle) ?? other.subtitle, lastMessageAt: - lastMessageAt?.merge(other.lastMessageAt) ?? other.lastMessageAt, + lastMessageAt?.merge(other.lastMessageAt) ?? other.lastMessageAt, avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme, unreadCounterColor: other.unreadCounterColor, ); } -} \ No newline at end of file +} diff --git a/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart index 862a9a9e..23402c57 100644 --- a/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart @@ -32,7 +32,7 @@ class GalleryHeaderTheme extends InheritedTheme { /// ``` static GalleryHeaderThemeData of(BuildContext context) { final galleryHeaderTheme = - context.dependOnInheritedWidgetOfExactType(); + context.dependOnInheritedWidgetOfExactType(); return galleryHeaderTheme?.data ?? StreamChatTheme.of(context).galleryHeaderTheme; } @@ -108,27 +108,27 @@ class GalleryHeaderThemeData with Diagnosticable { titleTextStyle: titleTextStyle ?? this.titleTextStyle, subtitleTextStyle: subtitleTextStyle ?? this.subtitleTextStyle, bottomSheetBarrierColor: - bottomSheetBarrierColor ?? this.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 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), + Color.lerp(a.iconMenuPointColor, b.iconMenuPointColor, t), titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t), subtitleTextStyle: - TextStyle.lerp(a.subtitleTextStyle, b.subtitleTextStyle, t), + TextStyle.lerp(a.subtitleTextStyle, b.subtitleTextStyle, t), bottomSheetBarrierColor: - Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t), + Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t), ); /// Merges one [GalleryHeaderThemeData] with the another @@ -147,14 +147,14 @@ class GalleryHeaderThemeData with Diagnosticable { @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; + 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 => diff --git a/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart index b8889e6b..09c99478 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart @@ -72,12 +72,12 @@ class MessageInputTheme { }) => MessageInputTheme( sendAnimationDuration: - sendAnimationDuration ?? this.sendAnimationDuration, + sendAnimationDuration ?? this.sendAnimationDuration, inputBackground: inputBackground ?? this.inputBackground, actionButtonColor: actionButtonColor ?? this.actionButtonColor, sendButtonColor: sendButtonColor ?? this.sendButtonColor, actionButtonIdleColor: - actionButtonIdleColor ?? this.actionButtonIdleColor, + actionButtonIdleColor ?? this.actionButtonIdleColor, expandButtonColor: expandButtonColor ?? this.expandButtonColor, inputTextStyle: inputTextStyle ?? this.inputTextStyle, sendButtonIdleColor: sendButtonIdleColor ?? this.sendButtonIdleColor, @@ -98,7 +98,7 @@ class MessageInputTheme { sendButtonColor: other.sendButtonColor, sendButtonIdleColor: other.sendButtonIdleColor, inputTextStyle: - inputTextStyle?.merge(other.inputTextStyle) ?? other.inputTextStyle, + inputTextStyle?.merge(other.inputTextStyle) ?? other.inputTextStyle, inputDecoration: inputDecoration?.merge(other.inputDecoration) ?? other.inputDecoration, activeBorderGradient: other.activeBorderGradient, @@ -107,4 +107,4 @@ class MessageInputTheme { expandButtonColor: other.expandButtonColor, ); } -} \ No newline at end of file +} diff --git a/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart index 49bd05f9..8ad8ea9d 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart @@ -32,7 +32,7 @@ class MessageListViewTheme extends InheritedTheme { /// ``` static MessageListViewThemeData of(BuildContext context) { final messageListViewTheme = - context.dependOnInheritedWidgetOfExactType(); + context.dependOnInheritedWidgetOfExactType(); return messageListViewTheme?.data ?? StreamChatTheme.of(context).messageListViewTheme; } @@ -77,10 +77,10 @@ class MessageListViewThemeData with Diagnosticable { /// /// All the properties must be non-null. MessageListViewThemeData lerp( - MessageListViewThemeData a, - MessageListViewThemeData b, - double t, - ) => + MessageListViewThemeData a, + MessageListViewThemeData b, + double t, + ) => MessageListViewThemeData( backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), ); @@ -96,9 +96,9 @@ class MessageListViewThemeData with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is MessageListViewThemeData && - runtimeType == other.runtimeType && - backgroundColor == other.backgroundColor; + other is MessageListViewThemeData && + runtimeType == other.runtimeType && + backgroundColor == other.backgroundColor; @override int get hashCode => backgroundColor.hashCode; diff --git a/packages/stream_chat_flutter/lib/src/theme/themes.dart b/packages/stream_chat_flutter/lib/src/theme/themes.dart index 4236f874..a3e974e8 100644 --- a/packages/stream_chat_flutter/lib/src/theme/themes.dart +++ b/packages/stream_chat_flutter/lib/src/theme/themes.dart @@ -12,4 +12,4 @@ export 'message_list_view_theme.dart'; export 'message_search_list_view_theme.dart'; export 'message_theme.dart'; export 'text_theme.dart'; -export 'user_list_view_theme.dart'; \ No newline at end of file +export 'user_list_view_theme.dart'; From 2e1bd3d2bef79f9ebc6eca40366ad44368398f0d Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 29 Jul 2021 18:22:55 +0530 Subject: [PATCH 011/121] added new guide --- .../docs/Flutter/assets/live_stream_1.jpg | Bin 0 -> 26070 bytes .../docs/Flutter/assets/live_stream_2.jpg | Bin 0 -> 30178 bytes .../adding_chat_to_video_livestreams.mdx | 76 ++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 docusaurus/docs/Flutter/assets/live_stream_1.jpg create mode 100644 docusaurus/docs/Flutter/assets/live_stream_2.jpg create mode 100644 docusaurus/docs/Flutter/guides/adding_chat_to_video_livestreams.mdx diff --git a/docusaurus/docs/Flutter/assets/live_stream_1.jpg b/docusaurus/docs/Flutter/assets/live_stream_1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bef68c6edb98f28f3b789a8ea9a15a3aa853181c GIT binary patch literal 26070 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 literal 0 HcmV?d00001 diff --git a/docusaurus/docs/Flutter/assets/live_stream_2.jpg b/docusaurus/docs/Flutter/assets/live_stream_2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..02a35132976c34f12e4ca177d7f80a59c34570bc GIT binary patch literal 30178 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 U1;#p%x(K-`k=XX#mwzk&4*=jNBLDyZ literal 0 HcmV?d00001 diff --git a/docusaurus/docs/Flutter/guides/adding_chat_to_video_livestreams.mdx b/docusaurus/docs/Flutter/guides/adding_chat_to_video_livestreams.mdx new file mode 100644 index 00000000..b5bdca6c --- /dev/null +++ b/docusaurus/docs/Flutter/guides/adding_chat_to_video_livestreams.mdx @@ -0,0 +1,76 @@ +--- +id: adding_chat_to_video_livestreams +sidebar_position: 7 +title: Adding Chat To Video Livestreams +--- + +Adding Chat To Video Livestreams + +### Introduction + +There are two common scenarios in live-streaming applications. Let's explore creating both types: + +### Split-screen + +In the split-screen implementation, we have a visual split between the video and the message list. + +![](../assets/live_stream_1.jpg) + +``` +Scaffold( + body: Column( + children: [ + Expanded( + child: // Your video implementation here, + ), + Expanded( + child: Column( + children: [ + Expanded( + child: MessageListView(), + ), + MessageInput(), + ], + ), + ), + ], + ), +) +``` + +### Overlapping chat with a transparency gradient + +The second type looks like this: + +![](../assets/live_stream_2.jpg) + +We can use a `Stack` for achieving this: + +``` +Scaffold( + body: Stack( + children: [ + // Add your video implementation here + ShaderMask( + shaderCallback: (rect) { + return LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [Colors.black, Colors.transparent], + stops: [0.4, 0.65] + ).createShader(Rect.fromLTRB(0, 0, rect.width, rect.height)); + }, + blendMode: BlendMode.dstIn, + child: Column( + children: [ + Expanded( + child: MessageListView(), + ), + MessageInput(), + ], + ), + ), + ], + ), + ) +``` From 15defc4caa308c719966d16e4f2f1a7429a2b107 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Thu, 29 Jul 2021 09:23:50 -0400 Subject: [PATCH 012/121] chore: remove unnecessary ChannelTheme class --- .../lib/src/channel_header.dart | 15 +++---- .../lib/src/channel_info.dart | 5 +-- .../lib/src/full_screen_media.dart | 1 - .../lib/src/message_list_view.dart | 2 +- .../lib/src/stream_chat_theme.dart | 44 +++++++++---------- .../lib/src/theme/channel_header_theme.dart | 2 +- .../lib/src/theme/channel_theme.dart | 28 ------------ .../lib/src/theme/themes.dart | 1 - .../lib/src/thread_header.dart | 11 +++-- 9 files changed, 36 insertions(+), 73 deletions(-) delete mode 100644 packages/stream_chat_flutter/lib/src/theme/channel_theme.dart diff --git a/packages/stream_chat_flutter/lib/src/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel_header.dart index 4df44765..c3adf9ee 100644 --- a/packages/stream_chat_flutter/lib/src/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_header.dart @@ -140,17 +140,16 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { brightness: Theme.of(context).brightness, elevation: 1, leading: leadingWidget, - backgroundColor: - chatThemeData.channelTheme.channelHeaderTheme.color, + backgroundColor: chatThemeData.channelHeaderTheme.color, actions: actions ?? [ Padding( padding: const EdgeInsets.only(right: 10), child: Center( child: ChannelAvatar( - borderRadius: chatThemeData.channelTheme + borderRadius: chatThemeData .channelHeaderTheme.avatarTheme?.borderRadius, - constraints: chatThemeData.channelTheme + constraints: chatThemeData .channelHeaderTheme.avatarTheme?.constraints, onTap: onImageTap, ), @@ -168,16 +167,16 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { children: [ title ?? ChannelName( - textStyle: chatThemeData - .channelTheme.channelHeaderTheme.titleStyle, + textStyle: + chatThemeData.channelHeaderTheme.titleStyle, ), const SizedBox(height: 2), subtitle ?? ChannelInfo( showTypingIndicator: showTypingIndicator, channel: channel, - textStyle: chatThemeData - .channelTheme.channelHeaderTheme.subtitleStyle, + textStyle: + chatThemeData.channelHeaderTheme.subtitleStyle, ), ], ), diff --git a/packages/stream_chat_flutter/lib/src/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel_info.dart index 67e18801..e7fa84c0 100644 --- a/packages/stream_chat_flutter/lib/src/channel_info.dart +++ b/packages/stream_chat_flutter/lib/src/channel_info.dart @@ -61,10 +61,7 @@ class ChannelInfo extends StatelessWidget { if (watcherCount > 0) text += ' $watcherCount Online'; alternativeWidget = Text( text, - style: StreamChatTheme.of(context) - .channelTheme - .channelHeaderTheme - .subtitleStyle, + style: StreamChatTheme.of(context).channelHeaderTheme.subtitleStyle, ); } else { final userId = StreamChat.of(context).currentUser?.id; diff --git a/packages/stream_chat_flutter/lib/src/full_screen_media.dart b/packages/stream_chat_flutter/lib/src/full_screen_media.dart index 0f6101b9..5d32d512 100644 --- a/packages/stream_chat_flutter/lib/src/full_screen_media.dart +++ b/packages/stream_chat_flutter/lib/src/full_screen_media.dart @@ -125,7 +125,6 @@ class _FullScreenMediaState extends State backgroundDecoration: BoxDecoration( color: ColorTween( begin: StreamChatTheme.of(context) - .channelTheme .channelHeaderTheme .color, end: Colors.black, diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 303c76db..bdc460af 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -628,7 +628,7 @@ class _MessageListViewState extends State { child: Text( '$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}', textAlign: TextAlign.center, - style: _streamTheme.channelTheme.channelHeaderTheme.subtitleStyle, + style: _streamTheme.channelHeaderTheme.subtitleStyle, ), ), ); diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index f83050a3..98a6e156 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -49,7 +49,7 @@ class StreamChatThemeData { ColorTheme? colorTheme, ChannelListHeaderTheme? channelListHeaderTheme, ChannelPreviewTheme? channelPreviewTheme, - ChannelTheme? channelTheme, + ChannelHeaderThemeData? channelHeaderTheme, MessageTheme? otherMessageTheme, MessageTheme? ownMessageTheme, MessageInputTheme? messageInputTheme, @@ -76,7 +76,7 @@ class StreamChatThemeData { final customizedData = defaultData.copyWith( channelListHeaderTheme: channelListHeaderTheme, channelPreviewTheme: channelPreviewTheme, - channelTheme: channelTheme, + channelHeaderTheme: channelHeaderTheme, otherMessageTheme: otherMessageTheme, ownMessageTheme: ownMessageTheme, messageInputTheme: messageInputTheme, @@ -108,7 +108,7 @@ class StreamChatThemeData { required this.colorTheme, required this.channelListHeaderTheme, required this.channelPreviewTheme, - required this.channelTheme, + required this.channelHeaderTheme, required this.otherMessageTheme, required this.ownMessageTheme, required this.messageInputTheme, @@ -147,8 +147,8 @@ class StreamChatThemeData { /// Theme of the [ChannelListHeader] final ChannelListHeaderTheme channelListHeaderTheme; - /// Theme of the chat widgets dedicated to a channel - final ChannelTheme channelTheme; + /// Theme of the chat widgets dedicated to a channel header + final ChannelHeaderThemeData channelHeaderTheme; /// The default style for [GalleryHeader]s below the overall /// [StreamChatTheme]. @@ -194,7 +194,7 @@ class StreamChatThemeData { TextTheme? textTheme, ColorTheme? colorTheme, ChannelPreviewTheme? channelPreviewTheme, - ChannelTheme? channelTheme, + ChannelHeaderThemeData? channelHeaderTheme, MessageTheme? ownMessageTheme, MessageTheme? otherMessageTheme, MessageInputTheme? messageInputTheme, @@ -218,7 +218,7 @@ class StreamChatThemeData { defaultUserImage: defaultUserImage ?? this.defaultUserImage, channelPreviewTheme: this.channelPreviewTheme.merge(channelPreviewTheme), - channelTheme: this.channelTheme.merge(channelTheme), + channelHeaderTheme: this.channelHeaderTheme.merge(channelHeaderTheme), ownMessageTheme: this.ownMessageTheme.merge(ownMessageTheme), otherMessageTheme: this.otherMessageTheme.merge(otherMessageTheme), messageInputTheme: this.messageInputTheme.merge(messageInputTheme), @@ -243,7 +243,7 @@ class StreamChatThemeData { primaryIconTheme: other.primaryIconTheme, defaultUserImage: other.defaultUserImage, channelPreviewTheme: channelPreviewTheme.merge(other.channelPreviewTheme), - channelTheme: channelTheme.merge(other.channelTheme), + channelHeaderTheme: channelHeaderTheme.merge(other.channelHeaderTheme), ownMessageTheme: ownMessageTheme.merge(other.ownMessageTheme), otherMessageTheme: otherMessageTheme.merge(other.otherMessageTheme), messageInputTheme: messageInputTheme.merge(other.messageInputTheme), @@ -269,21 +269,19 @@ class StreamChatThemeData { final accentColor = colorTheme.accentPrimary; final iconTheme = IconThemeData(color: colorTheme.textHighEmphasis.withOpacity(.5)); - final channelTheme = ChannelTheme( - channelHeaderTheme: ChannelHeaderThemeData( - avatarTheme: AvatarThemeData( - borderRadius: BorderRadius.circular(20), - constraints: const BoxConstraints.tightFor( - height: 40, - width: 40, - ), - ), - color: colorTheme.barsBg, - titleStyle: textTheme.headlineBold, - subtitleStyle: textTheme.footnote.copyWith( - color: const Color(0xff7A7A7A), + final channelHeaderTheme = ChannelHeaderThemeData( + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 40, + width: 40, ), ), + color: colorTheme.barsBg, + titleStyle: textTheme.headlineBold, + subtitleStyle: textTheme.footnote.copyWith( + color: const Color(0xff7A7A7A), + ), ); final channelPreviewTheme = ChannelPreviewTheme( unreadCounterColor: colorTheme.accentError, @@ -325,7 +323,7 @@ class StreamChatThemeData { color: colorTheme.barsBg, title: textTheme.headlineBold, ), - channelTheme: channelTheme, + channelHeaderTheme: channelHeaderTheme, ownMessageTheme: MessageTheme( messageAuthor: textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), @@ -459,7 +457,7 @@ class StreamChatThemeData { ], galleryHeaderTheme: GalleryHeaderThemeData( closeButtonColor: colorTheme.textHighEmphasis, - backgroundColor: channelTheme.channelHeaderTheme.color, + backgroundColor: channelHeaderTheme.color, iconMenuPointColor: colorTheme.textHighEmphasis, titleTextStyle: textTheme.headlineBold, subtitleTextStyle: channelPreviewTheme.subtitle, diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart index 2d3253dc..63e208e5 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart @@ -36,7 +36,7 @@ class ChannelHeaderTheme extends InheritedTheme { final channelHeaderTheme = context.dependOnInheritedWidgetOfExactType(); return channelHeaderTheme?.data ?? - StreamChatTheme.of(context).channelTheme.channelHeaderTheme; + StreamChatTheme.of(context).channelHeaderTheme; } @override diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_theme.dart deleted file mode 100644 index a8c6260d..00000000 --- a/packages/stream_chat_flutter/lib/src/theme/channel_theme.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'package:stream_chat_flutter/src/theme/channel_header_theme.dart'; - -/// Channel theme data -class ChannelTheme { - /// Constructor for creating [ChannelTheme] - ChannelTheme({ - required this.channelHeaderTheme, - }); - - /// Theme of the [ChannelHeader] widget - final ChannelHeaderThemeData channelHeaderTheme; - - /// Creates a copy of [ChannelTheme] with specified attributes overridden. - ChannelTheme copyWith({ - ChannelHeaderThemeData? channelHeaderTheme, - }) => - ChannelTheme( - channelHeaderTheme: channelHeaderTheme ?? this.channelHeaderTheme, - ); - - /// Merge with theme - ChannelTheme merge(ChannelTheme? other) { - if (other == null) return this; - return copyWith( - channelHeaderTheme: channelHeaderTheme.merge(other.channelHeaderTheme), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/theme/themes.dart b/packages/stream_chat_flutter/lib/src/theme/themes.dart index a3e974e8..7e2fb0aa 100644 --- a/packages/stream_chat_flutter/lib/src/theme/themes.dart +++ b/packages/stream_chat_flutter/lib/src/theme/themes.dart @@ -3,7 +3,6 @@ export 'channel_header_theme.dart'; export 'channel_list_header_theme.dart'; export 'channel_list_view_theme.dart'; export 'channel_preview_theme.dart'; -export 'channel_theme.dart'; export 'color_theme.dart'; export 'gallery_footer_theme.dart'; export 'gallery_header_theme.dart'; diff --git a/packages/stream_chat_flutter/lib/src/thread_header.dart b/packages/stream_chat_flutter/lib/src/thread_header.dart index 0473ca48..adf2f0e6 100644 --- a/packages/stream_chat_flutter/lib/src/thread_header.dart +++ b/packages/stream_chat_flutter/lib/src/thread_header.dart @@ -112,12 +112,11 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { children: [ Text( 'with ', - style: chatThemeData.channelTheme.channelHeaderTheme.subtitleStyle, + style: chatThemeData.channelHeaderTheme.subtitleStyle, ), Flexible( child: ChannelName( - textStyle: - chatThemeData.channelTheme.channelHeaderTheme.subtitleStyle, + textStyle: chatThemeData.channelHeaderTheme.subtitleStyle, ), ), ], @@ -136,7 +135,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { showUnreads: true, ) : const SizedBox()), - backgroundColor: chatThemeData.channelTheme.channelHeaderTheme.color, + backgroundColor: chatThemeData.channelHeaderTheme.color, centerTitle: true, actions: actions, title: InkWell( @@ -150,14 +149,14 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { title ?? Text( 'Thread Reply', - style: chatThemeData.channelTheme.channelHeaderTheme.titleStyle, + style: chatThemeData.channelHeaderTheme.titleStyle, ), const SizedBox(height: 2), if (showTypingIndicator) TypingIndicator( alignment: Alignment.center, channel: StreamChannel.of(context).channel, - style: chatThemeData.channelTheme.channelHeaderTheme.subtitleStyle, + style: chatThemeData.channelHeaderTheme.subtitleStyle, parentId: parent.id, alternativeWidget: defaultSubtitle, ) From 5afc274a6d8ff7091c700ffb048c47224f8af561 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Thu, 29 Jul 2021 10:48:52 -0400 Subject: [PATCH 013/121] chore: add lerp, hashcode, ==, and diagnosticable to message_input_theme.dart --- .../lib/src/message_actions_modal.dart | 3 +- .../lib/src/message_input.dart | 4 +- .../lib/src/stream_chat_theme.dart | 2 +- .../lib/src/theme/message_input_theme.dart | 92 +++++++++++++++++-- 4 files changed, 91 insertions(+), 10 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart index efaf0ded..11d96e77 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -562,7 +562,8 @@ class _MessageActionsModalState extends State { elevation: 2, clipBehavior: Clip.hardEdge, isScrollControlled: true, - backgroundColor: streamChatThemeData.messageInputTheme.inputBackground, + backgroundColor: + streamChatThemeData.messageInputTheme.inputBackgroundColor, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular(16), diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 5a4f6822..2030f47a 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -328,7 +328,7 @@ class MessageInputState extends State { Widget build(BuildContext context) { Widget child = DecoratedBox( decoration: BoxDecoration( - color: _streamChatTheme.messageInputTheme.inputBackground, + color: _streamChatTheme.messageInputTheme.inputBackgroundColor, ), child: SafeArea( child: GestureDetector( @@ -565,7 +565,7 @@ class MessageInputState extends State { child: DecoratedBox( decoration: BoxDecoration( borderRadius: _streamChatTheme.messageInputTheme.borderRadius, - color: _streamChatTheme.messageInputTheme.inputBackground, + color: _streamChatTheme.messageInputTheme.inputBackgroundColor, ), child: Column( mainAxisSize: MainAxisSize.min, diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index 98a6e156..3f9c39fe 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -378,7 +378,7 @@ class StreamChatThemeData { expandButtonColor: colorTheme.accentPrimary, sendButtonColor: colorTheme.accentPrimary, sendButtonIdleColor: colorTheme.disabled, - inputBackground: colorTheme.barsBg, + inputBackgroundColor: colorTheme.barsBg, inputTextStyle: textTheme.body, idleBorderGradient: LinearGradient( colors: [ diff --git a/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart index 09c99478..1d14c1b9 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart @@ -1,8 +1,9 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; /// Defines the theme dedicated to the [MessageInput] widget -class MessageInputTheme { +class MessageInputTheme with Diagnosticable { /// Returns a new [MessageInputTheme] const MessageInputTheme({ this.sendAnimationDuration, @@ -10,7 +11,7 @@ class MessageInputTheme { this.sendButtonColor, this.actionButtonIdleColor, this.sendButtonIdleColor, - this.inputBackground, + this.inputBackgroundColor, this.inputTextStyle, this.inputDecoration, this.activeBorderGradient, @@ -38,7 +39,7 @@ class MessageInputTheme { final Color? expandButtonColor; /// Background color of [MessageInput] - final Color? inputBackground; + final Color? inputBackgroundColor; /// TextStyle of [MessageInput] final TextStyle? inputTextStyle; @@ -58,7 +59,7 @@ class MessageInputTheme { /// Returns a new [MessageInputTheme] replacing some of its properties MessageInputTheme copyWith({ Duration? sendAnimationDuration, - Color? inputBackground, + Color? inputBackgroundColor, Color? actionButtonColor, Color? sendButtonColor, Color? actionButtonIdleColor, @@ -73,7 +74,7 @@ class MessageInputTheme { MessageInputTheme( sendAnimationDuration: sendAnimationDuration ?? this.sendAnimationDuration, - inputBackground: inputBackground ?? this.inputBackground, + inputBackgroundColor: inputBackgroundColor ?? this.inputBackgroundColor, actionButtonColor: actionButtonColor ?? this.actionButtonColor, sendButtonColor: sendButtonColor ?? this.sendButtonColor, actionButtonIdleColor: @@ -87,12 +88,40 @@ class MessageInputTheme { borderRadius: borderRadius ?? this.borderRadius, ); + /// Linearly interpolate from one [MessageInputTheme] to another. + MessageInputTheme lerp( + MessageInputTheme a, + MessageInputTheme b, + double t, + ) => + MessageInputTheme( + actionButtonColor: + Color.lerp(a.actionButtonColor, b.actionButtonColor, t), + actionButtonIdleColor: + Color.lerp(a.actionButtonIdleColor, b.actionButtonIdleColor, t), + activeBorderGradient: + Gradient.lerp(a.activeBorderGradient, b.activeBorderGradient, t), + borderRadius: BorderRadius.lerp(a.borderRadius, b.borderRadius, t), + expandButtonColor: + Color.lerp(a.expandButtonColor, b.expandButtonColor, t), + idleBorderGradient: + Gradient.lerp(a.idleBorderGradient, b.idleBorderGradient, t), + inputBackgroundColor: + Color.lerp(a.inputBackgroundColor, b.inputBackgroundColor, t), + inputTextStyle: TextStyle.lerp(a.inputTextStyle, b.inputTextStyle, t), + sendButtonColor: Color.lerp(a.sendButtonColor, b.sendButtonColor, t), + sendButtonIdleColor: + Color.lerp(a.sendButtonIdleColor, b.sendButtonIdleColor, t), + sendAnimationDuration: a.sendAnimationDuration, + inputDecoration: a.inputDecoration, + ); + /// Merges [this] [MessageInputTheme] with the [other] MessageInputTheme merge(MessageInputTheme? other) { if (other == null) return this; return copyWith( sendAnimationDuration: other.sendAnimationDuration, - inputBackground: other.inputBackground, + inputBackgroundColor: other.inputBackgroundColor, actionButtonColor: other.actionButtonColor, actionButtonIdleColor: other.actionButtonIdleColor, sendButtonColor: other.sendButtonColor, @@ -107,4 +136,55 @@ class MessageInputTheme { expandButtonColor: other.expandButtonColor, ); } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MessageInputTheme && + runtimeType == other.runtimeType && + sendAnimationDuration == other.sendAnimationDuration && + sendButtonColor == other.sendButtonColor && + actionButtonColor == other.actionButtonColor && + sendButtonIdleColor == other.sendButtonIdleColor && + actionButtonIdleColor == other.actionButtonIdleColor && + expandButtonColor == other.expandButtonColor && + inputBackgroundColor == other.inputBackgroundColor && + inputTextStyle == other.inputTextStyle && + inputDecoration == other.inputDecoration && + idleBorderGradient == other.idleBorderGradient && + activeBorderGradient == other.activeBorderGradient && + borderRadius == other.borderRadius; + + @override + int get hashCode => + sendAnimationDuration.hashCode ^ + sendButtonColor.hashCode ^ + actionButtonColor.hashCode ^ + sendButtonIdleColor.hashCode ^ + actionButtonIdleColor.hashCode ^ + expandButtonColor.hashCode ^ + inputBackgroundColor.hashCode ^ + inputTextStyle.hashCode ^ + inputDecoration.hashCode ^ + idleBorderGradient.hashCode ^ + activeBorderGradient.hashCode ^ + borderRadius.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('sendAnimationDuration', sendAnimationDuration)) + ..add(ColorProperty('inputBackgroundColor', inputBackgroundColor)) + ..add(ColorProperty('actionButtonColor', actionButtonColor)) + ..add(ColorProperty('actionButtonIdleColor', actionButtonIdleColor)) + ..add(ColorProperty('sendButtonColor', sendButtonColor)) + ..add(ColorProperty('sendButtonIdleColor', sendButtonIdleColor)) + ..add(DiagnosticsProperty('inputTextStyle', inputTextStyle)) + ..add(DiagnosticsProperty('inputDecoration', inputDecoration)) + ..add(DiagnosticsProperty('activeBorderGradient', activeBorderGradient)) + ..add(DiagnosticsProperty('idleBorderGradient', idleBorderGradient)) + ..add(DiagnosticsProperty('borderRadius', borderRadius)) + ..add(ColorProperty('expandButtonColor', expandButtonColor)); + } } From 8fb45ec4d1530361cd2f2008c0ec83c9a339f36a Mon Sep 17 00:00:00 2001 From: groovinchip Date: Thu, 29 Jul 2021 10:50:03 -0400 Subject: [PATCH 014/121] chore: rename MessageInputTheme to MessageInputThemeData --- .../lib/src/stream_chat_theme.dart | 8 +++--- .../lib/src/theme/message_input_theme.dart | 28 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index 3f9c39fe..c2eb8c85 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -52,7 +52,7 @@ class StreamChatThemeData { ChannelHeaderThemeData? channelHeaderTheme, MessageTheme? otherMessageTheme, MessageTheme? ownMessageTheme, - MessageInputTheme? messageInputTheme, + MessageInputThemeData? messageInputTheme, Widget Function(BuildContext, User)? defaultUserImage, IconThemeData? primaryIconTheme, List? reactionIcons, @@ -165,7 +165,7 @@ class StreamChatThemeData { final MessageTheme otherMessageTheme; /// Theme dedicated to the [MessageInput] widget - final MessageInputTheme messageInputTheme; + final MessageInputThemeData messageInputTheme; /// The widget that will be built when the user image is unavailable final Widget Function(BuildContext, User) defaultUserImage; @@ -197,7 +197,7 @@ class StreamChatThemeData { ChannelHeaderThemeData? channelHeaderTheme, MessageTheme? ownMessageTheme, MessageTheme? otherMessageTheme, - MessageInputTheme? messageInputTheme, + MessageInputThemeData? messageInputTheme, Widget Function(BuildContext, User)? defaultUserImage, IconThemeData? primaryIconTheme, ChannelListHeaderTheme? channelListHeaderTheme, @@ -370,7 +370,7 @@ class StreamChatThemeData { ), ), ), - messageInputTheme: MessageInputTheme( + messageInputTheme: MessageInputThemeData( borderRadius: BorderRadius.circular(20), sendAnimationDuration: const Duration(milliseconds: 300), actionButtonColor: colorTheme.accentPrimary, diff --git a/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart index 1d14c1b9..45f4a6b7 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart @@ -3,9 +3,9 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; /// Defines the theme dedicated to the [MessageInput] widget -class MessageInputTheme with Diagnosticable { - /// Returns a new [MessageInputTheme] - const MessageInputTheme({ +class MessageInputThemeData with Diagnosticable { + /// Returns a new [MessageInputThemeData] + const MessageInputThemeData({ this.sendAnimationDuration, this.actionButtonColor, this.sendButtonColor, @@ -56,8 +56,8 @@ class MessageInputTheme with Diagnosticable { /// Border radius of [MessageInput] final BorderRadius? borderRadius; - /// Returns a new [MessageInputTheme] replacing some of its properties - MessageInputTheme copyWith({ + /// Returns a new [MessageInputThemeData] replacing some of its properties + MessageInputThemeData copyWith({ Duration? sendAnimationDuration, Color? inputBackgroundColor, Color? actionButtonColor, @@ -71,7 +71,7 @@ class MessageInputTheme with Diagnosticable { Gradient? idleBorderGradient, BorderRadius? borderRadius, }) => - MessageInputTheme( + MessageInputThemeData( sendAnimationDuration: sendAnimationDuration ?? this.sendAnimationDuration, inputBackgroundColor: inputBackgroundColor ?? this.inputBackgroundColor, @@ -88,13 +88,13 @@ class MessageInputTheme with Diagnosticable { borderRadius: borderRadius ?? this.borderRadius, ); - /// Linearly interpolate from one [MessageInputTheme] to another. - MessageInputTheme lerp( - MessageInputTheme a, - MessageInputTheme b, + /// Linearly interpolate from one [MessageInputThemeData] to another. + MessageInputThemeData lerp( + MessageInputThemeData a, + MessageInputThemeData b, double t, ) => - MessageInputTheme( + MessageInputThemeData( actionButtonColor: Color.lerp(a.actionButtonColor, b.actionButtonColor, t), actionButtonIdleColor: @@ -116,8 +116,8 @@ class MessageInputTheme with Diagnosticable { inputDecoration: a.inputDecoration, ); - /// Merges [this] [MessageInputTheme] with the [other] - MessageInputTheme merge(MessageInputTheme? other) { + /// Merges [this] [MessageInputThemeData] with the [other] + MessageInputThemeData merge(MessageInputThemeData? other) { if (other == null) return this; return copyWith( sendAnimationDuration: other.sendAnimationDuration, @@ -140,7 +140,7 @@ class MessageInputTheme with Diagnosticable { @override bool operator ==(Object other) => identical(this, other) || - other is MessageInputTheme && + other is MessageInputThemeData && runtimeType == other.runtimeType && sendAnimationDuration == other.sendAnimationDuration && sendButtonColor == other.sendButtonColor && From 700ce9a7753573ff750a6c06cb313db42a15e779 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Thu, 29 Jul 2021 11:02:29 -0400 Subject: [PATCH 015/121] chore: add a MessageInputTheme inherited theme --- .../lib/src/theme/message_input_theme.dart | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart index 45f4a6b7..26a995a9 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart @@ -1,10 +1,57 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; -/// Defines the theme dedicated to the [MessageInput] widget +/// Overrides the default style of [MessageInput] descendants. +/// +/// See also: +/// +/// * [MessageInputThemeData], which is used to configure this theme. +class MessageInputTheme extends InheritedTheme { + /// Creates a [MessageInputTheme]. + /// + /// The [data] parameter must not be null. + const MessageInputTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final MessageInputThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [MessageInputTheme] widget, then + /// [StreamChatThemeData.messageInputTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// final theme = MessageInputTheme.of(context); + /// ``` + static MessageInputThemeData of(BuildContext context) { + final messageInputTheme = + context.dependOnInheritedWidgetOfExactType(); + return messageInputTheme?.data ?? + StreamChatTheme.of(context).messageInputTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) => + MessageInputTheme(data: data, child: child); + + @override + bool updateShouldNotify(MessageInputTheme oldWidget) => + data != oldWidget.data; +} + +/// A style that overrides the default appearance of [MessageInput] widgets +/// when used with [MessageInputTheme] or with the overall [StreamChatTheme]'s +/// [StreamChatThemeData.messageInputTheme]. class MessageInputThemeData with Diagnosticable { - /// Returns a new [MessageInputThemeData] + /// Creates a [MessageInputThemeData]. const MessageInputThemeData({ this.sendAnimationDuration, this.actionButtonColor, From 1aa62987994e02b9a6fca2c4323f0b52dfb7a9d7 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Thu, 29 Jul 2021 11:07:17 -0400 Subject: [PATCH 016/121] chore: use MessageInputTheme in message_input.dart --- .../lib/src/message_input.dart | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 2030f47a..30bfd747 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -288,6 +288,7 @@ class MessageInputState extends State { late final TextEditingController textEditingController; late StreamChatThemeData _streamChatTheme; + late MessageInputThemeData _messageInputTheme; bool get _hasQuotedMessage => widget.quotedMessage != null; @@ -328,7 +329,7 @@ class MessageInputState extends State { Widget build(BuildContext context) { Widget child = DecoratedBox( decoration: BoxDecoration( - color: _streamChatTheme.messageInputTheme.inputBackgroundColor, + color: _messageInputTheme.inputBackgroundColor, ), child: SafeArea( child: GestureDetector( @@ -484,7 +485,7 @@ class MessageInputState extends State { : CrossFadeState.showSecond, firstChild: sendButton, secondChild: widget.idleSendButton ?? _buildIdleSendButton(context), - duration: _streamChatTheme.messageInputTheme.sendAnimationDuration!, + duration: _messageInputTheme.sendAnimationDuration!, alignment: Alignment.center, ); } @@ -509,7 +510,7 @@ class MessageInputState extends State { ? pi : 0, child: StreamSvgIcon.emptyCircleLeft( - color: _streamChatTheme.messageInputTheme.expandButtonColor, + color: _messageInputTheme.expandButtonColor, ), ), padding: const EdgeInsets.all(0), @@ -555,17 +556,17 @@ class MessageInputState extends State { clipBehavior: Clip.hardEdge, margin: margin, decoration: BoxDecoration( - borderRadius: _streamChatTheme.messageInputTheme.borderRadius, + borderRadius: _messageInputTheme.borderRadius, gradient: _focusNode.hasFocus - ? _streamChatTheme.messageInputTheme.activeBorderGradient - : _streamChatTheme.messageInputTheme.idleBorderGradient, + ? _messageInputTheme.activeBorderGradient + : _messageInputTheme.idleBorderGradient, ), child: Padding( padding: const EdgeInsets.all(1.5), child: DecoratedBox( decoration: BoxDecoration( - borderRadius: _streamChatTheme.messageInputTheme.borderRadius, - color: _streamChatTheme.messageInputTheme.inputBackgroundColor, + borderRadius: _messageInputTheme.borderRadius, + color: _messageInputTheme.inputBackgroundColor, ), child: Column( mainAxisSize: MainAxisSize.min, @@ -583,7 +584,7 @@ class MessageInputState extends State { keyboardType: widget.keyboardType, controller: textEditingController, focusNode: _focusNode, - style: _streamChatTheme.messageInputTheme.inputTextStyle, + style: _messageInputTheme.inputTextStyle, autofocus: widget.autofocus, textAlignVertical: TextAlignVertical.center, decoration: _getInputDecoration(), @@ -599,11 +600,11 @@ class MessageInputState extends State { } InputDecoration _getInputDecoration() { - final passedDecoration = _streamChatTheme.messageInputTheme.inputDecoration; + final passedDecoration = _messageInputTheme.inputDecoration; return InputDecoration( isDense: true, hintText: _getHint(), - hintStyle: _streamChatTheme.messageInputTheme.inputTextStyle!.copyWith( + hintStyle: _messageInputTheme.inputTextStyle!.copyWith( color: _streamChatTheme.colorTheme.textLowEmphasis, ), border: const OutlineInputBorder( @@ -1685,8 +1686,8 @@ class MessageInputState extends State { color: s.isNotEmpty ? _streamChatTheme.colorTheme.disabled : (_commandsOverlay != null - ? _streamChatTheme.messageInputTheme.actionButtonColor - : _streamChatTheme.messageInputTheme.actionButtonIdleColor), + ? _messageInputTheme.actionButtonColor + : _messageInputTheme.actionButtonIdleColor), ), padding: const EdgeInsets.all(0), constraints: const BoxConstraints.tightFor( @@ -1723,8 +1724,8 @@ class MessageInputState extends State { Widget _buildAttachmentButton() => IconButton( icon: StreamSvgIcon.attach( color: _openFilePickerSection - ? _streamChatTheme.messageInputTheme.actionButtonColor - : _streamChatTheme.messageInputTheme.actionButtonIdleColor, + ? _messageInputTheme.actionButtonColor + : _messageInputTheme.actionButtonIdleColor, ), padding: const EdgeInsets.all(0), constraints: const BoxConstraints.tightFor( @@ -1959,7 +1960,7 @@ class MessageInputState extends State { padding: const EdgeInsets.all(8), child: StreamSvgIcon( assetName: _getIdleSendIcon(), - color: _streamChatTheme.messageInputTheme.sendButtonIdleColor, + color: _messageInputTheme.sendButtonIdleColor, ), ); @@ -1975,7 +1976,7 @@ class MessageInputState extends State { ), icon: StreamSvgIcon( assetName: _getSendIcon(), - color: _streamChatTheme.messageInputTheme.sendButtonColor, + color: _messageInputTheme.sendButtonColor, ), ), ); @@ -2182,6 +2183,7 @@ class MessageInputState extends State { @override void didChangeDependencies() { _streamChatTheme = StreamChatTheme.of(context); + _messageInputTheme = MessageInputTheme.of(context); if (widget.editMessage != null && !_initialized) { FocusScope.of(context).requestFocus(_focusNode); _initialized = true; From 501d646349b04153eb7d610a385fb8f3e76b36ca Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 29 Jul 2021 20:42:26 +0530 Subject: [PATCH 017/121] added details --- .../guides/adding_chat_to_video_livestreams.mdx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/docusaurus/docs/Flutter/guides/adding_chat_to_video_livestreams.mdx b/docusaurus/docs/Flutter/guides/adding_chat_to_video_livestreams.mdx index b5bdca6c..a34227a4 100644 --- a/docusaurus/docs/Flutter/guides/adding_chat_to_video_livestreams.mdx +++ b/docusaurus/docs/Flutter/guides/adding_chat_to_video_livestreams.mdx @@ -8,15 +8,23 @@ Adding Chat To Video Livestreams ### Introduction +Video livestreams are usually complemented with a chat section to make the livestream more interactive +and encourage retention. + +This guide details multiple ways of adding chat functionality to your video livestream. + +### Implementing Chat + There are two common scenarios in live-streaming applications. Let's explore creating both types: ### Split-screen In the split-screen implementation, we have a visual split between the video and the message list. +This allows the content to be unobstructed by chat and have a clear separation of boundaries. ![](../assets/live_stream_1.jpg) -``` +```dart Scaffold( body: Column( children: [ @@ -40,13 +48,17 @@ Scaffold( ### Overlapping chat with a transparency gradient +Another way to add chat is to overlay the video content with messages which progressively fade out +as we go to the top of the screen. This gives the content a more rich feel as it takes the whole +screen and allows the chat to be more homogeneously integrated with the content. + The second type looks like this: ![](../assets/live_stream_2.jpg) We can use a `Stack` for achieving this: -``` +```dart Scaffold( body: Stack( children: [ From 822b0ad725abff8d85708640859689cd4d4062eb Mon Sep 17 00:00:00 2001 From: groovinchip Date: Thu, 29 Jul 2021 13:59:02 -0400 Subject: [PATCH 018/121] test: start testing MessageInputTheme --- .../src/theme/message_input_theme_test.dart | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart diff --git a/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart new file mode 100644 index 00000000..b1427509 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart' hide TextTheme; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + test('MessageInputThemeData copyWith, ==, hashCode basics', () { + expect(const MessageInputThemeData(), + const MessageInputThemeData().copyWith()); + expect(const MessageInputThemeData().hashCode, + const MessageInputThemeData().copyWith().hashCode); + }); + + group('MessageInputThemeData lerps correctly', () { + test('Lerp completely from light to dark', () { + expect( + const MessageInputThemeData().lerp( + _messageInputThemeControl, _messageInputThemeControlDark, 1), + _messageInputThemeControlDark); + }); + }); +} + +final _messageInputThemeControl = MessageInputThemeData( + borderRadius: BorderRadius.circular(20), + sendAnimationDuration: const Duration(milliseconds: 300), + actionButtonColor: ColorTheme.light().accentPrimary, + actionButtonIdleColor: ColorTheme.light().textLowEmphasis, + expandButtonColor: ColorTheme.light().accentPrimary, + sendButtonColor: ColorTheme.light().accentPrimary, + sendButtonIdleColor: ColorTheme.light().disabled, + inputBackgroundColor: ColorTheme.light().barsBg, + inputTextStyle: TextTheme.light().body, + idleBorderGradient: LinearGradient( + colors: [ + ColorTheme.light().disabled, + ColorTheme.light().disabled, + ], + ), + activeBorderGradient: LinearGradient( + colors: [ + ColorTheme.light().disabled, + ColorTheme.light().disabled, + ], + ), +); + +final _messageInputThemeControlMidLerp = MessageInputThemeData( + borderRadius: BorderRadius.circular(20), + sendAnimationDuration: const Duration(milliseconds: 300), + actionButtonColor: ColorTheme.light().accentPrimary, + actionButtonIdleColor: ColorTheme.light().textLowEmphasis, + expandButtonColor: ColorTheme.light().accentPrimary, + sendButtonColor: ColorTheme.light().accentPrimary, + sendButtonIdleColor: ColorTheme.light().disabled, + inputBackgroundColor: ColorTheme.light().barsBg, + inputTextStyle: TextTheme.light().body, + idleBorderGradient: LinearGradient( + colors: [ + ColorTheme.light().disabled, + ColorTheme.light().disabled, + ], + ), + activeBorderGradient: LinearGradient( + colors: [ + ColorTheme.light().disabled, + ColorTheme.light().disabled, + ], + ), +); + +final _messageInputThemeControlDark = MessageInputThemeData( + borderRadius: BorderRadius.circular(20), + sendAnimationDuration: const Duration(milliseconds: 300), + actionButtonColor: ColorTheme.dark().accentPrimary, + actionButtonIdleColor: ColorTheme.dark().textLowEmphasis, + expandButtonColor: ColorTheme.dark().accentPrimary, + sendButtonColor: ColorTheme.dark().accentPrimary, + sendButtonIdleColor: ColorTheme.dark().disabled, + inputBackgroundColor: ColorTheme.dark().barsBg, + inputTextStyle: TextTheme.dark().body, + idleBorderGradient: LinearGradient( + colors: [ + ColorTheme.dark().disabled, + ColorTheme.dark().disabled, + ], + ), + activeBorderGradient: LinearGradient( + colors: [ + ColorTheme.dark().disabled, + ColorTheme.dark().disabled, + ], + ), +); From 419864cd908083617f4f642cd1a64088d9dab288 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 30 Jul 2021 11:08:21 +0200 Subject: [PATCH 019/121] docs(localization): minor fix --- docusaurus/docs/Flutter/guides/adding_localization.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docusaurus/docs/Flutter/guides/adding_localization.mdx b/docusaurus/docs/Flutter/guides/adding_localization.mdx index 7e6cb7e2..c08f081c 100644 --- a/docusaurus/docs/Flutter/guides/adding_localization.mdx +++ b/docusaurus/docs/Flutter/guides/adding_localization.mdx @@ -40,7 +40,7 @@ Then run `flutter packages get` ### Usage -Generally, Flutter and the Stream Chat SDK will use the system locale of the user's device, if that locale is supported (see below). If the locale is not supported we will default to `en`. +Generally, Flutter and the Stream Chat SDK will use the system locale of the user's device, if that locale is supported (see below). If the locale is not supported we will default to `en` (however it's always possible to [customize that](#changing-the-default-language)). Make sure to read more about localization in the [official Flutter docs](https://flutter.dev/docs/development/accessibility-and-localization/internationalization). ```dart From 7561125a057eef0d7da50ffdb67113fecaf08b0b Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 30 Jul 2021 14:57:26 +0530 Subject: [PATCH 020/121] fix(ui): add null-check in currentUserMember Signed-off-by: xsahil03x --- packages/stream_chat_flutter/lib/src/message_list_view.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 5ae6f9ea..17ad24c6 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -965,7 +965,7 @@ class _MessageListViewState extends State { final currentUser = StreamChat.of(context).currentUser; final members = StreamChannel.of(context).channel.state?.members ?? []; final currentUserMember = - members.firstWhere((e) => e.user!.id == currentUser!.id); + members.firstWhereOrNull((e) => e.user!.id == currentUser!.id); Widget messageWidget = MessageWidget( key: ValueKey('MESSAGE-${message.id}'), @@ -1072,7 +1072,8 @@ class _MessageListViewState extends State { } FocusScope.of(context).unfocus(); }, - showPinButton: widget.pinPermissions.contains(currentUserMember.role), + showPinButton: currentUserMember != null && + widget.pinPermissions.contains(currentUserMember.role), ); if (widget.messageBuilder != null) { From 0837c5576d2f6e6e41daaa3955d4ec9b1523556d Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 30 Jul 2021 15:03:19 +0530 Subject: [PATCH 021/121] chore(ui): update changelog Signed-off-by: xsahil03x --- packages/stream_chat_flutter/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index de55a3ca..27110e50 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +🐞 Fixed + +- [#590](https://github.com/GetStream/stream-chat-flutter/issues/590): livestream use case, no members when sending message + ## 2.1.1 - Updated core dependency From 8399a97c2952713f3616fc885c2243b7c4d9c2ef Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 30 Jul 2021 15:38:36 +0530 Subject: [PATCH 022/121] added details --- .../Flutter/guides/adding_chat_to_video_livestreams.mdx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docusaurus/docs/Flutter/guides/adding_chat_to_video_livestreams.mdx b/docusaurus/docs/Flutter/guides/adding_chat_to_video_livestreams.mdx index a34227a4..9e2afb44 100644 --- a/docusaurus/docs/Flutter/guides/adding_chat_to_video_livestreams.mdx +++ b/docusaurus/docs/Flutter/guides/adding_chat_to_video_livestreams.mdx @@ -9,13 +9,18 @@ Adding Chat To Video Livestreams ### Introduction Video livestreams are usually complemented with a chat section to make the livestream more interactive -and encourage retention. +and encourage retention. There are several ways to show the chat interface on the screen and requires +some design choices. This guide details multiple ways of adding chat functionality to your video livestream. ### Implementing Chat -There are two common scenarios in live-streaming applications. Let's explore creating both types: +There are two common scenarios in live-streaming applications depending how well integrated the two +components (video + chat) are allowed to be on the screen. Two common types are split-screen and a +chat overlay that fades in. + +Let's explore creating both types: ### Split-screen From be24604e5395d153e9e3a96dd02183588c45dbfa Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 30 Jul 2021 17:57:45 +0530 Subject: [PATCH 023/121] added added new guide --- .../guides/customize_message_actions.mdx | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 docusaurus/docs/Flutter/guides/customize_message_actions.mdx diff --git a/docusaurus/docs/Flutter/guides/customize_message_actions.mdx b/docusaurus/docs/Flutter/guides/customize_message_actions.mdx new file mode 100644 index 00000000..10b79fc1 --- /dev/null +++ b/docusaurus/docs/Flutter/guides/customize_message_actions.mdx @@ -0,0 +1,79 @@ +--- +id: customize_message_actions +sidebar_position: 8 +title: Customize Message Actions +--- + +Customizing Message Actions + +### Introduction + +Message actions pop up in message overlay, when you long-press a message. +We have provided granular control over these actions. + +By default we render the following message actions: + +* edit message + +* delete message + +* reply + +* thread reply + +* copy message + +* flag message + +* pin message + +:::note +Edit and delete message are only available on messages sent by the user. +Additionally, pinning a message requires you to add the roles which are allowed to pin messages. +::: + +### Partially remove some message actions + +For example, if you only want to keep "copy message" and "delete message", +here is how to do it using the `messageBuilder` with our `MessageWidget`. + +```dart +MessageListView( + messageBuilder: (context, details, messages, defaultMessage) { + return defaultMessage.copyWith( + showFlagButton: false, + showEditMessage: false, + showCopyMessage: true, + showDeleteMessage: details.isMyMessage, + showReplyMessage: false, + showThreadReplyMessage: false, + ); + }, +) +``` + +### Add a new custom message action + +The SDK also allows you to add new actions into the dialog. + +For example, let's suppose you want to introduce a new message action - "Demo Action": + +We use the `customActions` parameter of the `MessageWidget` to add extra actions. + +```dart +MessageListView( + messageBuilder: (context, details, messages, defaultMessage) { + return MessageWidget( + customActions: [ + MessageAction( + leading: Icon(Icons.add), + title: Text('Demo Action'), + onTap: (message) { + /// Complete action here + }, + ), + ], + ); + }, +) +``` From bfcf827787f6afa25c4afc433fd35d608ef7c7f9 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 30 Jul 2021 18:06:18 +0530 Subject: [PATCH 024/121] added image --- .../docs/Flutter/assets/message_actions.png | Bin 0 -> 123514 bytes .../guides/customize_message_actions.mdx | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 docusaurus/docs/Flutter/assets/message_actions.png diff --git a/docusaurus/docs/Flutter/assets/message_actions.png b/docusaurus/docs/Flutter/assets/message_actions.png new file mode 100644 index 0000000000000000000000000000000000000000..157f39791fa05143e10a3911a1566e8988b53a06 GIT binary patch literal 123514 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

    YD=5uh~ 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 literal 0 HcmV?d00001 diff --git a/docusaurus/docs/Flutter/guides/customize_message_actions.mdx b/docusaurus/docs/Flutter/guides/customize_message_actions.mdx index 10b79fc1..90237558 100644 --- a/docusaurus/docs/Flutter/guides/customize_message_actions.mdx +++ b/docusaurus/docs/Flutter/guides/customize_message_actions.mdx @@ -9,6 +9,9 @@ Customizing Message Actions ### Introduction Message actions pop up in message overlay, when you long-press a message. + +![](../assets/message_actions.png) + We have provided granular control over these actions. By default we render the following message actions: From 1148fc7d6270ed0de39f8c747ce46b5c27b5fdb7 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 30 Jul 2021 18:16:49 +0530 Subject: [PATCH 025/121] fixed snippet --- docusaurus/docs/Flutter/guides/customize_message_actions.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docusaurus/docs/Flutter/guides/customize_message_actions.mdx b/docusaurus/docs/Flutter/guides/customize_message_actions.mdx index 90237558..b1cc07a0 100644 --- a/docusaurus/docs/Flutter/guides/customize_message_actions.mdx +++ b/docusaurus/docs/Flutter/guides/customize_message_actions.mdx @@ -66,7 +66,7 @@ We use the `customActions` parameter of the `MessageWidget` to add extra actions ```dart MessageListView( messageBuilder: (context, details, messages, defaultMessage) { - return MessageWidget( + return defaultMessage.copyWith( customActions: [ MessageAction( leading: Icon(Icons.add), From 6a32a293119325d95f49cd7feee87452c026f143 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 30 Jul 2021 16:08:56 +0200 Subject: [PATCH 026/121] chore(ui): 2.1.2 --- packages/stream_chat_flutter/CHANGELOG.md | 2 +- packages/stream_chat_flutter/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 27110e50..81e6235e 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,4 +1,4 @@ -## Upcoming +## 2.1.2 🐞 Fixed diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index f47fa9f5..61146b72 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter. -version: 2.1.1 +version: 2.1.2 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues From 4586bfc6eb40c3cc96096bcd65b3a25409bc9c1b Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 30 Jul 2021 16:13:49 +0200 Subject: [PATCH 027/121] chore(repo): update pana conditions --- .github/workflows/pana.yml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pana.yml b/.github/workflows/pana.yml index bef2ec03..a6a7e822 100644 --- a/.github/workflows/pana.yml +++ b/.github/workflows/pana.yml @@ -31,8 +31,7 @@ jobs: TOTAL: ${{ steps.analysis.outputs.total }} TOTAL_MAX: ${{ steps.analysis.outputs.total_max }} run: | - PERCENTAGE=$(( $TOTAL * 100 / $TOTAL_MAX )) - if (( $PERCENTAGE < 90 )) + if (( $TOTAL < 120 )) then echo Score too low! exit 1 @@ -53,7 +52,7 @@ jobs: TOTAL_MAX: ${{ steps.analysis.outputs.total_max }} run: | PERCENTAGE=$(( $TOTAL * 100 / $TOTAL_MAX )) - if (( $PERCENTAGE < 90 )) + if (( $TOTAL < 90 )) then echo Score too low! exit 1 @@ -73,8 +72,7 @@ jobs: TOTAL: ${{ steps.analysis.outputs.total }} TOTAL_MAX: ${{ steps.analysis.outputs.total_max }} run: | - PERCENTAGE=$(( $TOTAL * 100 / $TOTAL_MAX )) - if (( $PERCENTAGE < 100 )) + if (( $TOTAL < 120 )) then echo Score too low! exit 1 @@ -94,8 +92,7 @@ jobs: TOTAL: ${{ steps.analysis.outputs.total }} TOTAL_MAX: ${{ steps.analysis.outputs.total_max }} run: | - PERCENTAGE=$(( $TOTAL * 100 / $TOTAL_MAX )) - if (( $PERCENTAGE < 80 )) + if (( $TOTAL < 100 )) then echo Score too low! exit 1 From cc89edc2444b4136cf72dc1a531ce3bab72d8889 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Fri, 30 Jul 2021 10:30:33 -0400 Subject: [PATCH 028/121] test: fix light to dark test, add dark to light test --- .../test/src/theme/message_input_theme_test.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart index b1427509..b19d5dee 100644 --- a/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart @@ -17,6 +17,13 @@ void main() { _messageInputThemeControl, _messageInputThemeControlDark, 1), _messageInputThemeControlDark); }); + + test('Lerp completely from dark to light', () { + expect( + const MessageInputThemeData().lerp( + _messageInputThemeControlDark, _messageInputThemeControl, 1), + _messageInputThemeControl); + }); }); } @@ -79,12 +86,14 @@ final _messageInputThemeControlDark = MessageInputThemeData( inputBackgroundColor: ColorTheme.dark().barsBg, inputTextStyle: TextTheme.dark().body, idleBorderGradient: LinearGradient( + stops: const [0.0, 1.0], colors: [ ColorTheme.dark().disabled, ColorTheme.dark().disabled, ], ), activeBorderGradient: LinearGradient( + stops: const [0.0, 1.0], colors: [ ColorTheme.dark().disabled, ColorTheme.dark().disabled, From f0f2418e92bb28bed4d17ad8020053f324ff7e10 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 30 Jul 2021 20:39:10 +0530 Subject: [PATCH 029/121] test(ui): fix lerp dark to light test Signed-off-by: Sahil Kumar --- .../test/src/theme/message_input_theme_test.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart index b19d5dee..227cdc9c 100644 --- a/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart @@ -38,12 +38,14 @@ final _messageInputThemeControl = MessageInputThemeData( inputBackgroundColor: ColorTheme.light().barsBg, inputTextStyle: TextTheme.light().body, idleBorderGradient: LinearGradient( + stops: const [0.0, 1.0], colors: [ ColorTheme.light().disabled, ColorTheme.light().disabled, ], ), activeBorderGradient: LinearGradient( + stops: const [0.0, 1.0], colors: [ ColorTheme.light().disabled, ColorTheme.light().disabled, From 5b99507db350121e5ab52a2b075a65f0b5e8fcc5 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Fri, 30 Jul 2021 11:34:46 -0400 Subject: [PATCH 030/121] test: lerp halfway from light to dark --- .../src/theme/message_input_theme_test.dart | 39 ++++++++++++------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart index 227cdc9c..69e9669a 100644 --- a/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart @@ -18,6 +18,13 @@ void main() { _messageInputThemeControlDark); }); + test('Lerp halfway from light to dark', () { + expect( + const MessageInputThemeData().lerp( + _messageInputThemeControl, _messageInputThemeControlDark, 0.5), + _messageInputThemeControlMidLerp); + }); + test('Lerp completely from dark to light', () { expect( const MessageInputThemeData().lerp( @@ -56,23 +63,29 @@ final _messageInputThemeControl = MessageInputThemeData( final _messageInputThemeControlMidLerp = MessageInputThemeData( borderRadius: BorderRadius.circular(20), sendAnimationDuration: const Duration(milliseconds: 300), - actionButtonColor: ColorTheme.light().accentPrimary, - actionButtonIdleColor: ColorTheme.light().textLowEmphasis, - expandButtonColor: ColorTheme.light().accentPrimary, - sendButtonColor: ColorTheme.light().accentPrimary, - sendButtonIdleColor: ColorTheme.light().disabled, - inputBackgroundColor: ColorTheme.light().barsBg, - inputTextStyle: TextTheme.light().body, - idleBorderGradient: LinearGradient( + inputBackgroundColor: const Color(0xff87898b), + actionButtonColor: const Color(0xff005fff), + actionButtonIdleColor: const Color(0xff7a7a7a), + sendButtonColor: const Color(0xff005fff), + sendButtonIdleColor: const Color(0xff848585), + expandButtonColor: const Color(0xff005fff), + inputTextStyle: const TextStyle( + color: Color(0xff7f7f7f), + fontSize: 14, + fontWeight: FontWeight.w500, + ), + idleBorderGradient: const LinearGradient( + stops: [0.0, 1.0], colors: [ - ColorTheme.light().disabled, - ColorTheme.light().disabled, + Color(0xff848585), + Color(0xff848585), ], ), - activeBorderGradient: LinearGradient( + activeBorderGradient: const LinearGradient( + stops: [0.0, 1.0], colors: [ - ColorTheme.light().disabled, - ColorTheme.light().disabled, + Color(0xff848585), + Color(0xff848585), ], ), ); From c7d399704e4c6614c9ed012289b6184254a69e12 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Fri, 30 Jul 2021 11:35:32 -0400 Subject: [PATCH 031/121] test: Merging two MessageInputThemeData results in the latter --- .../test/src/theme/message_input_theme_test.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart index 69e9669a..c618d65c 100644 --- a/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart @@ -32,6 +32,11 @@ void main() { _messageInputThemeControl); }); }); + + test('Merging two MessageInputThemeData results in the latter', () { + expect(_messageInputThemeControl.merge(_messageInputThemeControlDark), + _messageInputThemeControlDark); + }); } final _messageInputThemeControl = MessageInputThemeData( From 67658541329c9a4fd8fe64a07e9bbe9b6a3aff1c Mon Sep 17 00:00:00 2001 From: groovinchip Date: Fri, 30 Jul 2021 13:43:22 -0400 Subject: [PATCH 032/121] chore: rename ChannelPreviewTheme.title and .subtitle to .titleStyle and .subtitleStyle --- .../example/lib/tutorial_part_3.dart | 2 +- .../lib/src/channel_bottom_sheet.dart | 2 +- .../lib/src/channel_preview.dart | 14 +++++++------- .../lib/src/message_search_item.dart | 10 +++++----- .../lib/src/stream_chat_theme.dart | 6 +++--- .../lib/src/theme/channel_preview_theme.dart | 16 ++++++++-------- 6 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart index 9658eec4..b6214ff9 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart @@ -119,7 +119,7 @@ class ChannelListPage extends StatelessWidget { ), title: ChannelName( textStyle: - StreamChatTheme.of(context).channelPreviewTheme.title!.copyWith( + StreamChatTheme.of(context).channelPreviewTheme.titleStyle!.copyWith( color: StreamChatTheme.of(context) .colorTheme .textHighEmphasis diff --git a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart index 0d40c67a..a9b8a2f7 100644 --- a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart +++ b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart @@ -64,7 +64,7 @@ class _ChannelBottomSheetState extends State { showTypingIndicator: false, channel: _streamChannelState.channel, textStyle: - _streamChatThemeData.channelPreviewTheme.subtitle, + _streamChatThemeData.channelPreviewTheme.subtitleStyle, ), ), const SizedBox( diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index 81d09e3c..74c46ca5 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -90,7 +90,7 @@ class ChannelPreview extends StatelessWidget { Flexible( child: title ?? ChannelName( - textStyle: channelPreviewTheme.title, + textStyle: channelPreviewTheme.titleStyle, ), ), BetterStreamBuilder?>( @@ -199,7 +199,7 @@ class ChannelPreview extends StatelessWidget { ), Text( ' Channel is muted', - style: chatThemeData.channelPreviewTheme.subtitle, + style: chatThemeData.channelPreviewTheme.subtitleStyle, ), ], ); @@ -207,7 +207,7 @@ class ChannelPreview extends StatelessWidget { return TypingIndicator( channel: channel, alternativeWidget: _buildLastMessage(context), - style: chatThemeData.channelPreviewTheme.subtitle, + style: chatThemeData.channelPreviewTheme.subtitleStyle, ); } @@ -248,13 +248,13 @@ class ChannelPreview extends StatelessWidget { text, lastMessage.mentionedUsers, lastMessage.attachments, - chatThemeData.channelPreviewTheme.subtitle?.copyWith( - color: chatThemeData.channelPreviewTheme.subtitle?.color, + chatThemeData.channelPreviewTheme.subtitleStyle?.copyWith( + color: chatThemeData.channelPreviewTheme.subtitleStyle?.color, fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) ? FontStyle.italic : FontStyle.normal), - chatThemeData.channelPreviewTheme.subtitle?.copyWith( - color: chatThemeData.channelPreviewTheme.subtitle?.color, + chatThemeData.channelPreviewTheme.subtitleStyle?.copyWith( + color: chatThemeData.channelPreviewTheme.subtitleStyle?.color, fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) ? FontStyle.italic : FontStyle.normal, diff --git a/packages/stream_chat_flutter/lib/src/message_search_item.dart b/packages/stream_chat_flutter/lib/src/message_search_item.dart index d44ddd0d..9bb0ffd1 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_item.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_item.dart @@ -52,18 +52,18 @@ class MessageSearchItem extends StatelessWidget { user.id == StreamChat.of(context).currentUser?.id ? 'You' : user.name, - style: chatThemeData.channelPreviewTheme.title, + style: chatThemeData.channelPreviewTheme.titleStyle, ), if (channelName != null) ...[ Text( ' in ', - style: chatThemeData.channelPreviewTheme.title?.copyWith( + style: chatThemeData.channelPreviewTheme.titleStyle?.copyWith( fontWeight: FontWeight.normal, ), ), Text( channelName as String, - style: chatThemeData.channelPreviewTheme.title, + style: chatThemeData.channelPreviewTheme.titleStyle, ), ], ], @@ -127,12 +127,12 @@ class MessageSearchItem extends StatelessWidget { text!, message.mentionedUsers, message.attachments, - chatThemeData.channelPreviewTheme.subtitle?.copyWith( + chatThemeData.channelPreviewTheme.subtitleStyle?.copyWith( fontStyle: (message.isSystem || message.isDeleted) ? FontStyle.italic : FontStyle.normal, ), - chatThemeData.channelPreviewTheme.subtitle?.copyWith( + chatThemeData.channelPreviewTheme.subtitleStyle?.copyWith( fontStyle: (message.isSystem || message.isDeleted) ? FontStyle.italic : FontStyle.normal, diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index c2eb8c85..000f5aab 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -292,8 +292,8 @@ class StreamChatThemeData { width: 40, ), ), - title: textTheme.bodyBold, - subtitle: textTheme.footnote.copyWith( + titleStyle: textTheme.bodyBold, + subtitleStyle: textTheme.footnote.copyWith( color: const Color(0xff7A7A7A), ), lastMessageAt: textTheme.footnote.copyWith( @@ -460,7 +460,7 @@ class StreamChatThemeData { backgroundColor: channelHeaderTheme.color, iconMenuPointColor: colorTheme.textHighEmphasis, titleTextStyle: textTheme.headlineBold, - subtitleTextStyle: channelPreviewTheme.subtitle, + subtitleTextStyle: channelPreviewTheme.subtitleStyle, bottomSheetBarrierColor: colorTheme.overlay, ), galleryFooterTheme: GalleryFooterThemeData( diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart index 2fcecaf7..ceeb3c16 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart @@ -5,8 +5,8 @@ import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; class ChannelPreviewTheme { /// Constructor for creating [ChannelPreviewTheme] const ChannelPreviewTheme({ - this.title, - this.subtitle, + this.titleStyle, + this.subtitleStyle, this.lastMessageAt, this.avatarTheme, this.unreadCounterColor, @@ -14,10 +14,10 @@ class ChannelPreviewTheme { }); /// Theme for title - final TextStyle? title; + final TextStyle? titleStyle; /// Theme for subtitle - final TextStyle? subtitle; + final TextStyle? subtitleStyle; /// Theme of last message at final TextStyle? lastMessageAt; @@ -41,8 +41,8 @@ class ChannelPreviewTheme { double? indicatorIconSize, }) => ChannelPreviewTheme( - title: title ?? this.title, - subtitle: subtitle ?? this.subtitle, + titleStyle: title ?? this.titleStyle, + subtitleStyle: subtitle ?? this.subtitleStyle, lastMessageAt: lastMessageAt ?? this.lastMessageAt, avatarTheme: avatarTheme ?? this.avatarTheme, unreadCounterColor: unreadCounterColor ?? this.unreadCounterColor, @@ -53,8 +53,8 @@ class ChannelPreviewTheme { 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, + title: titleStyle?.merge(other.titleStyle) ?? other.titleStyle, + subtitle: subtitleStyle?.merge(other.subtitleStyle) ?? other.subtitleStyle, lastMessageAt: lastMessageAt?.merge(other.lastMessageAt) ?? other.lastMessageAt, avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme, From 019bd5d5da18f48cfc6595fdfb21b75c697bb5fb Mon Sep 17 00:00:00 2001 From: groovinchip Date: Fri, 30 Jul 2021 13:44:53 -0400 Subject: [PATCH 033/121] chore: run dartfmt on changed files --- .../example/lib/tutorial_part_3.dart | 16 +++++++++------- .../lib/src/channel_preview.dart | 3 ++- .../lib/src/theme/channel_preview_theme.dart | 3 ++- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart index b6214ff9..a6e80b57 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart @@ -118,13 +118,15 @@ class ChannelListPage extends StatelessWidget { channel: channel, ), title: ChannelName( - textStyle: - StreamChatTheme.of(context).channelPreviewTheme.titleStyle!.copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .textHighEmphasis - .withOpacity(opacity), - ), + textStyle: StreamChatTheme.of(context) + .channelPreviewTheme + .titleStyle! + .copyWith( + color: StreamChatTheme.of(context) + .colorTheme + .textHighEmphasis + .withOpacity(opacity), + ), ), subtitle: Text(subtitle), trailing: channel.state!.unreadCount > 0 diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index 74c46ca5..2a9b82da 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -249,7 +249,8 @@ class ChannelPreview extends StatelessWidget { lastMessage.mentionedUsers, lastMessage.attachments, chatThemeData.channelPreviewTheme.subtitleStyle?.copyWith( - color: chatThemeData.channelPreviewTheme.subtitleStyle?.color, + color: + chatThemeData.channelPreviewTheme.subtitleStyle?.color, fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) ? FontStyle.italic : FontStyle.normal), diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart index ceeb3c16..673061e9 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart @@ -54,7 +54,8 @@ class ChannelPreviewTheme { if (other == null) return this; return copyWith( title: titleStyle?.merge(other.titleStyle) ?? other.titleStyle, - subtitle: subtitleStyle?.merge(other.subtitleStyle) ?? other.subtitleStyle, + subtitle: + subtitleStyle?.merge(other.subtitleStyle) ?? other.subtitleStyle, lastMessageAt: lastMessageAt?.merge(other.lastMessageAt) ?? other.lastMessageAt, avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme, From f55110c458a1014dccda093c3fde9df30691c140 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Fri, 30 Jul 2021 13:45:58 -0400 Subject: [PATCH 034/121] fix: instances where new naming wasn't applied --- .../lib/src/theme/channel_preview_theme.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart index 673061e9..bb7a638f 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart @@ -33,16 +33,16 @@ class ChannelPreviewTheme { /// Copy with theme ChannelPreviewTheme copyWith({ - TextStyle? title, - TextStyle? subtitle, + TextStyle? titleStyle, + TextStyle? subtitleStyle, TextStyle? lastMessageAt, AvatarThemeData? avatarTheme, Color? unreadCounterColor, double? indicatorIconSize, }) => ChannelPreviewTheme( - titleStyle: title ?? this.titleStyle, - subtitleStyle: subtitle ?? this.subtitleStyle, + titleStyle: titleStyle ?? this.titleStyle, + subtitleStyle: subtitleStyle ?? this.subtitleStyle, lastMessageAt: lastMessageAt ?? this.lastMessageAt, avatarTheme: avatarTheme ?? this.avatarTheme, unreadCounterColor: unreadCounterColor ?? this.unreadCounterColor, @@ -53,8 +53,8 @@ class ChannelPreviewTheme { ChannelPreviewTheme merge(ChannelPreviewTheme? other) { if (other == null) return this; return copyWith( - title: titleStyle?.merge(other.titleStyle) ?? other.titleStyle, - subtitle: + titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle, + subtitleStyle: subtitleStyle?.merge(other.subtitleStyle) ?? other.subtitleStyle, lastMessageAt: lastMessageAt?.merge(other.lastMessageAt) ?? other.lastMessageAt, From 008dc0b1df909e7a13af64a8fdff4aeb78dcbd2b Mon Sep 17 00:00:00 2001 From: groovinchip Date: Fri, 30 Jul 2021 13:48:49 -0400 Subject: [PATCH 035/121] chore: add diagnosticable properties to channel_preview_theme.dart --- .../lib/src/theme/channel_preview_theme.dart | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart index bb7a638f..af3b35b5 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart @@ -1,8 +1,9 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; /// Theme for channel preview -class ChannelPreviewTheme { +class ChannelPreviewTheme with Diagnosticable { /// Constructor for creating [ChannelPreviewTheme] const ChannelPreviewTheme({ this.titleStyle, @@ -62,4 +63,15 @@ class ChannelPreviewTheme { unreadCounterColor: other.unreadCounterColor, ); } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('titleStyle', titleStyle)) + ..add(DiagnosticsProperty('subtitleStyle', subtitleStyle)) + ..add(DiagnosticsProperty('lastMessageAtStyle', lastMessageAt)) + ..add(DiagnosticsProperty('avatarTheme', avatarTheme)) + ..add(ColorProperty('unreadCounterColor', unreadCounterColor)); + } } From c5d53de53c5948af885cd2da6d8fdec28f8e5ee2 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Fri, 30 Jul 2021 13:50:42 -0400 Subject: [PATCH 036/121] chore: rename ChannelPreviewTheme.lastMessageAt to .lastMessageAtStyle --- .../lib/src/channel_preview.dart | 5 +++-- .../lib/src/message_search_item.dart | 2 +- .../lib/src/stream_chat_theme.dart | 2 +- .../lib/src/theme/channel_preview_theme.dart | 14 +++++++------- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index 2a9b82da..8a9eb9f1 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -182,8 +182,9 @@ class ChannelPreview extends StatelessWidget { return Text( stringDate, - style: - StreamChatTheme.of(context).channelPreviewTheme.lastMessageAt, + style: StreamChatTheme.of(context) + .channelPreviewTheme + .lastMessageAtStyle, ); }, ); diff --git a/packages/stream_chat_flutter/lib/src/message_search_item.dart b/packages/stream_chat_flutter/lib/src/message_search_item.dart index 9bb0ffd1..c465fb44 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_item.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_item.dart @@ -93,7 +93,7 @@ class MessageSearchItem extends StatelessWidget { return Text( stringDate, - style: StreamChatTheme.of(context).channelPreviewTheme.lastMessageAt, + style: StreamChatTheme.of(context).channelPreviewTheme.lastMessageAtStyle, ); } diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index 000f5aab..b323833a 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -296,7 +296,7 @@ class StreamChatThemeData { subtitleStyle: textTheme.footnote.copyWith( color: const Color(0xff7A7A7A), ), - lastMessageAt: textTheme.footnote.copyWith( + lastMessageAtStyle: textTheme.footnote.copyWith( color: colorTheme.textHighEmphasis.withOpacity(.5), ), indicatorIconSize: 16, diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart index af3b35b5..9f264750 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart @@ -8,7 +8,7 @@ class ChannelPreviewTheme with Diagnosticable { const ChannelPreviewTheme({ this.titleStyle, this.subtitleStyle, - this.lastMessageAt, + this.lastMessageAtStyle, this.avatarTheme, this.unreadCounterColor, this.indicatorIconSize, @@ -21,7 +21,7 @@ class ChannelPreviewTheme with Diagnosticable { final TextStyle? subtitleStyle; /// Theme of last message at - final TextStyle? lastMessageAt; + final TextStyle? lastMessageAtStyle; /// Avatar theme final AvatarThemeData? avatarTheme; @@ -36,7 +36,7 @@ class ChannelPreviewTheme with Diagnosticable { ChannelPreviewTheme copyWith({ TextStyle? titleStyle, TextStyle? subtitleStyle, - TextStyle? lastMessageAt, + TextStyle? lastMessageAtStyle, AvatarThemeData? avatarTheme, Color? unreadCounterColor, double? indicatorIconSize, @@ -44,7 +44,7 @@ class ChannelPreviewTheme with Diagnosticable { ChannelPreviewTheme( titleStyle: titleStyle ?? this.titleStyle, subtitleStyle: subtitleStyle ?? this.subtitleStyle, - lastMessageAt: lastMessageAt ?? this.lastMessageAt, + lastMessageAtStyle: lastMessageAtStyle ?? this.lastMessageAtStyle, avatarTheme: avatarTheme ?? this.avatarTheme, unreadCounterColor: unreadCounterColor ?? this.unreadCounterColor, indicatorIconSize: indicatorIconSize ?? this.indicatorIconSize, @@ -57,8 +57,8 @@ class ChannelPreviewTheme with Diagnosticable { titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle, subtitleStyle: subtitleStyle?.merge(other.subtitleStyle) ?? other.subtitleStyle, - lastMessageAt: - lastMessageAt?.merge(other.lastMessageAt) ?? other.lastMessageAt, + lastMessageAtStyle: lastMessageAtStyle?.merge(other.lastMessageAtStyle) ?? + other.lastMessageAtStyle, avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme, unreadCounterColor: other.unreadCounterColor, ); @@ -70,7 +70,7 @@ class ChannelPreviewTheme with Diagnosticable { properties ..add(DiagnosticsProperty('titleStyle', titleStyle)) ..add(DiagnosticsProperty('subtitleStyle', subtitleStyle)) - ..add(DiagnosticsProperty('lastMessageAtStyle', lastMessageAt)) + ..add(DiagnosticsProperty('lastMessageAtStyle', lastMessageAtStyle)) ..add(DiagnosticsProperty('avatarTheme', avatarTheme)) ..add(ColorProperty('unreadCounterColor', unreadCounterColor)); } From 37f4ee720e1ad2d045f4645172302709bf5babc8 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Fri, 30 Jul 2021 13:52:07 -0400 Subject: [PATCH 037/121] chore: rename ChannelPreviewTheme to ChannelPreviewThemeData --- .../example/lib/tutorial_part_6.dart | 2 +- .../lib/src/stream_chat_theme.dart | 8 ++++---- .../lib/src/theme/channel_preview_theme.dart | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart index 1cf34d27..2cad67f2 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart @@ -58,7 +58,7 @@ class MyApp extends StatelessWidget { final defaultTheme = StreamChatThemeData.fromTheme(themeData); final colorTheme = defaultTheme.colorTheme; final customTheme = defaultTheme.merge(StreamChatThemeData( - channelPreviewTheme: ChannelPreviewTheme( + channelPreviewTheme: ChannelPreviewThemeData( avatarTheme: AvatarThemeData( borderRadius: BorderRadius.circular(8), ), diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index b323833a..7a520f40 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -48,7 +48,7 @@ class StreamChatThemeData { TextTheme? textTheme, ColorTheme? colorTheme, ChannelListHeaderTheme? channelListHeaderTheme, - ChannelPreviewTheme? channelPreviewTheme, + ChannelPreviewThemeData? channelPreviewTheme, ChannelHeaderThemeData? channelHeaderTheme, MessageTheme? otherMessageTheme, MessageTheme? ownMessageTheme, @@ -142,7 +142,7 @@ class StreamChatThemeData { final ColorTheme colorTheme; /// Theme of the [ChannelPreview] - final ChannelPreviewTheme channelPreviewTheme; + final ChannelPreviewThemeData channelPreviewTheme; /// Theme of the [ChannelListHeader] final ChannelListHeaderTheme channelListHeaderTheme; @@ -193,7 +193,7 @@ class StreamChatThemeData { StreamChatThemeData copyWith({ TextTheme? textTheme, ColorTheme? colorTheme, - ChannelPreviewTheme? channelPreviewTheme, + ChannelPreviewThemeData? channelPreviewTheme, ChannelHeaderThemeData? channelHeaderTheme, MessageTheme? ownMessageTheme, MessageTheme? otherMessageTheme, @@ -283,7 +283,7 @@ class StreamChatThemeData { color: const Color(0xff7A7A7A), ), ); - final channelPreviewTheme = ChannelPreviewTheme( + final channelPreviewTheme = ChannelPreviewThemeData( unreadCounterColor: colorTheme.accentError, avatarTheme: AvatarThemeData( borderRadius: BorderRadius.circular(20), diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart index 9f264750..023c1f0a 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart @@ -3,9 +3,9 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; /// Theme for channel preview -class ChannelPreviewTheme with Diagnosticable { - /// Constructor for creating [ChannelPreviewTheme] - const ChannelPreviewTheme({ +class ChannelPreviewThemeData with Diagnosticable { + /// Constructor for creating [ChannelPreviewThemeData] + const ChannelPreviewThemeData({ this.titleStyle, this.subtitleStyle, this.lastMessageAtStyle, @@ -33,7 +33,7 @@ class ChannelPreviewTheme with Diagnosticable { final double? indicatorIconSize; /// Copy with theme - ChannelPreviewTheme copyWith({ + ChannelPreviewThemeData copyWith({ TextStyle? titleStyle, TextStyle? subtitleStyle, TextStyle? lastMessageAtStyle, @@ -41,7 +41,7 @@ class ChannelPreviewTheme with Diagnosticable { Color? unreadCounterColor, double? indicatorIconSize, }) => - ChannelPreviewTheme( + ChannelPreviewThemeData( titleStyle: titleStyle ?? this.titleStyle, subtitleStyle: subtitleStyle ?? this.subtitleStyle, lastMessageAtStyle: lastMessageAtStyle ?? this.lastMessageAtStyle, @@ -51,7 +51,7 @@ class ChannelPreviewTheme with Diagnosticable { ); /// Merge with theme - ChannelPreviewTheme merge(ChannelPreviewTheme? other) { + ChannelPreviewThemeData merge(ChannelPreviewThemeData? other) { if (other == null) return this; return copyWith( titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle, From d8ca465b91a496fa5bf23a53f05291aa02da611f Mon Sep 17 00:00:00 2001 From: groovinchip Date: Mon, 2 Aug 2021 09:24:17 -0400 Subject: [PATCH 038/121] chore: add lerp, == and hashCode to ChannelPreviewTheme --- .../lib/src/theme/channel_preview_theme.dart | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart index 023c1f0a..18783d02 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart @@ -50,6 +50,24 @@ class ChannelPreviewThemeData with Diagnosticable { indicatorIconSize: indicatorIconSize ?? this.indicatorIconSize, ); + /// Linearly interpolate one [ChannelPreviewThemeData] to another. + ChannelPreviewThemeData lerp( + ChannelPreviewThemeData a, + ChannelPreviewThemeData b, + double t, + ) => + ChannelPreviewThemeData( + avatarTheme: + const AvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), + indicatorIconSize: a.indicatorIconSize, + lastMessageAtStyle: + TextStyle.lerp(a.lastMessageAtStyle, b.lastMessageAtStyle, t), + subtitleStyle: TextStyle.lerp(a.subtitleStyle, b.subtitleStyle, t), + titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t), + unreadCounterColor: + Color.lerp(a.unreadCounterColor, b.unreadCounterColor, t), + ); + /// Merge with theme ChannelPreviewThemeData merge(ChannelPreviewThemeData? other) { if (other == null) return this; @@ -64,6 +82,27 @@ class ChannelPreviewThemeData with Diagnosticable { ); } + @override + bool operator ==(Object other) => + identical(this, other) || + other is ChannelPreviewThemeData && + runtimeType == other.runtimeType && + titleStyle == other.titleStyle && + subtitleStyle == other.subtitleStyle && + lastMessageAtStyle == other.lastMessageAtStyle && + avatarTheme == other.avatarTheme && + unreadCounterColor == other.unreadCounterColor && + indicatorIconSize == other.indicatorIconSize; + + @override + int get hashCode => + titleStyle.hashCode ^ + subtitleStyle.hashCode ^ + lastMessageAtStyle.hashCode ^ + avatarTheme.hashCode ^ + unreadCounterColor.hashCode ^ + indicatorIconSize.hashCode; + @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); From a06e6075d7f8f626eacd47784b2c062c95815d76 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Mon, 2 Aug 2021 09:38:15 -0400 Subject: [PATCH 039/121] chore: add ChannelPreviewTheme inherited theme and documentation --- .../lib/src/theme/channel_preview_theme.dart | 57 ++++++++++++++++++- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart index 18783d02..ff9ef70c 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart @@ -1,10 +1,63 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; -/// Theme for channel preview +/// Overrides the default style of [ChannelPreview] descendants. +/// +/// See also: +/// +/// * [ChannelPreviewThemeData], which is used to configure this theme. +class ChannelPreviewTheme extends InheritedTheme { + /// Creates a [ChannelPreviewTheme]. + /// + /// The [data] parameter must not be null. + const ChannelPreviewTheme({ + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final ChannelPreviewThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [ChannelPreviewTheme] widget, then + /// [StreamChatThemeData.channelPreviewTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// final theme = ChannelPreviewTheme.of(context); + /// ``` + static ChannelPreviewThemeData of(BuildContext context) { + final channelPreviewTheme = + context.dependOnInheritedWidgetOfExactType(); + return channelPreviewTheme?.data ?? + StreamChatTheme.of(context).channelPreviewTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) => + ChannelPreviewTheme(data: data, child: child); + + @override + bool updateShouldNotify(ChannelPreviewTheme oldWidget) => + data != oldWidget.data; +} + +/// A style that overrides the default appearance of [ChannelPreview]s when used +/// with [ChannelPreviewTheme] or with the overall [StreamChatTheme]'s +/// [StreamChatThemeData.channelPreviewTheme]. +/// +/// See also: +/// +/// * [ChannelPreviewTheme], the theme which is configured with this class. +/// * [StreamChatThemeData.channelPreviewTheme], which can be used to override +/// the default style for [ChannelHeader]s below the overall [StreamChatTheme]. class ChannelPreviewThemeData with Diagnosticable { - /// Constructor for creating [ChannelPreviewThemeData] + /// Creates a [ChannelPreviewThemeData]. const ChannelPreviewThemeData({ this.titleStyle, this.subtitleStyle, From 5e1eb7a32072f33e4371047b501a678c6e702c38 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Mon, 2 Aug 2021 09:53:39 -0400 Subject: [PATCH 040/121] test: add tests for ChannelPreviewTheme --- .../src/theme/channel_preview_theme_test.dart | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 packages/stream_chat_flutter/test/src/theme/channel_preview_theme_test.dart diff --git a/packages/stream_chat_flutter/test/src/theme/channel_preview_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/channel_preview_theme_test.dart new file mode 100644 index 00000000..24ad716b --- /dev/null +++ b/packages/stream_chat_flutter/test/src/theme/channel_preview_theme_test.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart' hide TextTheme; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + test('ChannelPreviewThemeData copyWith, ==, hashCode basics', () { + expect(const ChannelPreviewThemeData(), + const ChannelPreviewThemeData().copyWith()); + expect(const ChannelPreviewThemeData().hashCode, + const ChannelPreviewThemeData().copyWith().hashCode); + }); + + group('ChannelPreviewThemeData lerps', () { + test( + '''Light ChannelPreviewThemeData lerps completely to dark ChannelPreviewThemeData''', + () { + expect( + const ChannelPreviewThemeData().lerp( + _channelPreviewThemeControl, _channelPreviewThemeControlDark, 1), + _channelPreviewThemeControlDark); + }); + + test( + '''Light ChannelPreviewThemeData lerps halfway to dark ChannelPreviewThemeData''', + () { + expect( + const ChannelPreviewThemeData().lerp(_channelPreviewThemeControl, + _channelPreviewThemeControlDark, 0.5), + _channelPreviewThemeControlMidLerp); + }); + + test( + '''Dark ChannelPreviewThemeData lerps completely to light ChannelPreviewThemeData''', + () { + expect( + const ChannelPreviewThemeData().lerp( + _channelPreviewThemeControlDark, _channelPreviewThemeControl, 1), + _channelPreviewThemeControl); + }); + }); + + test('Merging dark and light themes results in a dark theme', () { + expect(_channelPreviewThemeControl.merge(_channelPreviewThemeControlDark), + _channelPreviewThemeControlDark); + }); +} + +final _channelPreviewThemeControl = ChannelPreviewThemeData( + unreadCounterColor: ColorTheme.light().accentError, + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + titleStyle: TextTheme.light().bodyBold, + subtitleStyle: TextTheme.light().footnote.copyWith( + color: const Color(0xff7A7A7A), + ), + lastMessageAtStyle: TextTheme.light().footnote.copyWith( + color: ColorTheme.light().textHighEmphasis.withOpacity(.5), + ), + indicatorIconSize: 16, +); + +final _channelPreviewThemeControlMidLerp = ChannelPreviewThemeData( + unreadCounterColor: const Color(0xffff3842), + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + titleStyle: const TextStyle( + color: Color(0xff000000), + fontSize: 14, + fontWeight: FontWeight.bold, + ), + subtitleStyle: const TextStyle( + color: Color(0xff7a7a7a), + fontSize: 12, + ), + lastMessageAtStyle: TextTheme.light().footnote.copyWith( + color: Color(0x80000000).withOpacity(.5), + ), + indicatorIconSize: 16, +); + +final _channelPreviewThemeControlDark = ChannelPreviewThemeData( + unreadCounterColor: ColorTheme.dark().accentError, + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + titleStyle: TextTheme.dark().bodyBold, + subtitleStyle: TextTheme.dark().footnote.copyWith( + color: const Color(0xff7A7A7A), + ), + lastMessageAtStyle: TextTheme.dark().footnote.copyWith( + color: ColorTheme.dark().textHighEmphasis.withOpacity(.5), + ), + indicatorIconSize: 16, +); From 628ef401d4154bc96b63ecad3f8b196898f39da6 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Mon, 2 Aug 2021 10:47:38 -0400 Subject: [PATCH 041/121] chore: update message_theme.dart Includes Diagnosticable, more descriptive field names, lerp, ==, and hashCode --- .../example/lib/tutorial_part_6.dart | 4 +- .../lib/src/attachment/attachment_title.dart | 6 +- .../lib/src/attachment/image_attachment.dart | 4 +- .../lib/src/attachment/video_attachment.dart | 4 +- .../lib/src/deleted_message.dart | 6 +- .../lib/src/image_group.dart | 4 +- .../lib/src/message_actions_modal.dart | 6 +- .../lib/src/message_reactions_modal.dart | 9 +- .../lib/src/message_text.dart | 18 +-- .../lib/src/message_widget.dart | 20 +-- .../lib/src/quoted_message_widget.dart | 6 +- .../lib/src/stream_chat_theme.dart | 36 ++--- .../lib/src/theme/message_theme.dart | 143 ++++++++++++++---- .../test/src/deleted_message_test.dart | 6 +- 14 files changed, 175 insertions(+), 97 deletions(-) diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart index 2cad67f2..b64281cb 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart @@ -63,9 +63,9 @@ class MyApp extends StatelessWidget { borderRadius: BorderRadius.circular(8), ), ), - otherMessageTheme: MessageTheme( + otherMessageTheme: MessageThemeData( messageBackgroundColor: colorTheme.textHighEmphasis, - messageText: TextStyle( + messageTextStyle: TextStyle( color: colorTheme.barsBg, ), avatarTheme: AvatarThemeData( diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart index d0f45aad..a7d4d2e3 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart @@ -13,7 +13,7 @@ class AttachmentTitle extends StatelessWidget { }) : super(key: key); /// Theme to apply to text - final MessageTheme messageTheme; + final MessageThemeData messageTheme; /// Attachment data to display final Attachment attachment; @@ -35,7 +35,7 @@ class AttachmentTitle extends StatelessWidget { Text( attachment.title!, overflow: TextOverflow.ellipsis, - style: messageTheme.messageText?.copyWith( + style: messageTheme.messageTextStyle?.copyWith( color: StreamChatTheme.of(context).colorTheme.accentPrimary, fontWeight: FontWeight.bold, ), @@ -51,7 +51,7 @@ class AttachmentTitle extends StatelessWidget { .toList() .reversed .join('.'), - style: messageTheme.messageText, + style: messageTheme.messageTextStyle, ), ], ), diff --git a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart index f912f282..95229418 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart @@ -28,8 +28,8 @@ class ImageAttachment extends AttachmentWidget { size: size, ); - /// [MessageTheme] for showing image title - final MessageTheme messageTheme; + /// [MessageThemeData] for showing image title + final MessageThemeData messageTheme; /// Flag for showing title final bool showTitle; diff --git a/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart index b976532a..ce4ac804 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart @@ -25,8 +25,8 @@ class VideoAttachment extends AttachmentWidget { size: size, ); - /// [MessageTheme] for showing title - final MessageTheme messageTheme; + /// [MessageThemeData] for showing title + final MessageThemeData messageTheme; /// Callback when show message is tapped final ShowMessageCallback? onShowMessage; diff --git a/packages/stream_chat_flutter/lib/src/deleted_message.dart b/packages/stream_chat_flutter/lib/src/deleted_message.dart index a973deb7..74673dff 100644 --- a/packages/stream_chat_flutter/lib/src/deleted_message.dart +++ b/packages/stream_chat_flutter/lib/src/deleted_message.dart @@ -15,7 +15,7 @@ class DeletedMessage extends StatelessWidget { }) : super(key: key); /// The theme of the message - final MessageTheme messageTheme; + final MessageThemeData messageTheme; /// The border radius of the message text final BorderRadiusGeometry? borderRadiusGeometry; @@ -51,9 +51,9 @@ class DeletedMessage extends StatelessWidget { ), child: Text( 'Message deleted', - style: messageTheme.messageText?.copyWith( + style: messageTheme.messageTextStyle?.copyWith( fontStyle: FontStyle.italic, - color: messageTheme.createdAt?.color, + color: messageTheme.createdAtStyle?.color, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/image_group.dart b/packages/stream_chat_flutter/lib/src/image_group.dart index ec50f294..627406d0 100644 --- a/packages/stream_chat_flutter/lib/src/image_group.dart +++ b/packages/stream_chat_flutter/lib/src/image_group.dart @@ -26,8 +26,8 @@ class ImageGroup extends StatelessWidget { /// Message which images are attached to final Message message; - /// [MessageTheme] to apply to message - final MessageTheme messageTheme; + /// [MessageThemeData] to apply to message + final MessageThemeData messageTheme; /// Size of iamges final Size size; diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart index 11d96e77..2c7e163d 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -50,8 +50,8 @@ class MessageActionsModal extends StatefulWidget { /// Message in focus for actions final Message message; - /// [MessageTheme] for message - final MessageTheme messageTheme; + /// [MessageThemeData] for message + final MessageThemeData messageTheme; /// Flag for showing reactions final bool showReactions; @@ -117,7 +117,7 @@ class _MessageActionsModalState extends State { } } final roughSentenceSize = messageTextLength * - (widget.messageTheme.messageText?.fontSize ?? 1) * + (widget.messageTheme.messageTextStyle?.fontSize ?? 1) * 1.2; final divFactor = widget.message.attachments.isNotEmpty == true ? 1 diff --git a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart index 669af376..5be0189a 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -28,8 +28,8 @@ class MessageReactionsModal extends StatelessWidget { /// Message to display reactions of final Message message; - /// [MessageTheme] to apply to [message] - final MessageTheme messageTheme; + /// [MessageThemeData] to apply to [message] + final MessageThemeData messageTheme; /// Flag to reverse message final bool reverse; @@ -56,8 +56,9 @@ class MessageReactionsModal extends StatelessWidget { messageTextLength = quotedMessageLength; } } - final roughSentenceSize = - messageTextLength * (messageTheme.messageText?.fontSize ?? 1) * 1.2; + final roughSentenceSize = messageTextLength * + (messageTheme.messageTextStyle?.fontSize ?? 1) * + 1.2; final divFactor = message.attachments.isNotEmpty == true ? 1 : (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize)); diff --git a/packages/stream_chat_flutter/lib/src/message_text.dart b/packages/stream_chat_flutter/lib/src/message_text.dart index c11dc2fc..964a32a7 100644 --- a/packages/stream_chat_flutter/lib/src/message_text.dart +++ b/packages/stream_chat_flutter/lib/src/message_text.dart @@ -25,8 +25,8 @@ class MessageText extends StatelessWidget { /// Callback for when link is tapped final void Function(String)? onLinkTap; - /// [MessageTheme] whose text theme is to be applied - final MessageTheme messageTheme; + /// [MessageThemeData] whose text theme is to be applied + final MessageThemeData messageTheme; @override Widget build(BuildContext context) { @@ -64,16 +64,16 @@ class MessageText extends StatelessWidget { styleSheet: MarkdownStyleSheet.fromTheme( themeData.copyWith( textTheme: themeData.textTheme.apply( - bodyColor: messageTheme.messageText?.color, - decoration: messageTheme.messageText?.decoration, - decorationColor: messageTheme.messageText?.decorationColor, - decorationStyle: messageTheme.messageText?.decorationStyle, - fontFamily: messageTheme.messageText?.fontFamily, + bodyColor: messageTheme.messageTextStyle?.color, + decoration: messageTheme.messageTextStyle?.decoration, + decorationColor: messageTheme.messageTextStyle?.decorationColor, + decorationStyle: messageTheme.messageTextStyle?.decorationStyle, + fontFamily: messageTheme.messageTextStyle?.fontFamily, ), ), ).copyWith( - a: messageTheme.messageLinks, - p: messageTheme.messageText, + a: messageTheme.messageLinksStyle, + p: messageTheme.messageTextStyle, ), ); } diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 133b5dc7..d218855d 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -283,7 +283,7 @@ class MessageWidget extends StatefulWidget { final Message message; /// The message theme - final MessageTheme messageTheme; + final MessageThemeData messageTheme; /// If true the widget will be mirrored final bool reverse; @@ -413,7 +413,7 @@ class MessageWidget extends StatefulWidget { Widget Function(BuildContext, Message)? usernameBuilder, void Function(BuildContext, Message)? onMessageActions, Message? message, - MessageTheme? messageTheme, + MessageThemeData? messageTheme, bool? reverse, ShapeBorder? shape, ShapeBorder? attachmentShape, @@ -888,14 +888,14 @@ class _MessageWidgetState extends State ), InkWell( onTap: widget.onThreadTap != null ? onThreadTap : null, - child: Text(msg, style: widget.messageTheme.replies), + child: Text(msg, style: widget.messageTheme.repliesStyle), ), ], if (showUsername) _buildUsername(usernameKey), if (showTimeStamp) Text( Jiffy(widget.message.createdAt.toLocal()).jm, - style: widget.messageTheme.createdAt, + style: widget.messageTheme.createdAtStyle, ), if (showSendingIndicator) _buildSendingIndicator(), ]); @@ -912,7 +912,7 @@ class _MessageWidgetState extends State Container( margin: EdgeInsets.only( bottom: context.textScaleFactor * - ((widget.messageTheme.replies?.fontSize ?? 1) / 2), + ((widget.messageTheme.repliesStyle?.fontSize ?? 1) / 2), ), child: CustomPaint( size: const Size(16, 32) * context.textScaleFactor, @@ -939,7 +939,7 @@ class _MessageWidgetState extends State Container( margin: EdgeInsets.only( bottom: context.textScaleFactor * - ((widget.messageTheme.replies?.fontSize ?? 1) / 2), + ((widget.messageTheme.repliesStyle?.fontSize ?? 1) / 2), ), child: CustomPaint( size: const Size(16, 32) * context.textScaleFactor, @@ -962,7 +962,7 @@ class _MessageWidgetState extends State widget.message.user!.name, maxLines: 1, key: usernameKey, - style: widget.messageTheme.messageAuthor, + style: widget.messageTheme.messageAuthorStyle, overflow: TextOverflow.ellipsis, ); } @@ -1186,7 +1186,7 @@ class _MessageWidgetState extends State } Widget _buildSendingIndicator() { - final style = widget.messageTheme.createdAt; + final style = widget.messageTheme.createdAtStyle; final message = widget.message; if (hasNonUrlAttachments && @@ -1263,8 +1263,8 @@ class _MessageWidgetState extends State onMentionTap: widget.onMentionTap, messageTheme: isOnlyEmoji ? widget.messageTheme.copyWith( - messageText: - widget.messageTheme.messageText!.copyWith( + messageTextStyle: + widget.messageTheme.messageTextStyle!.copyWith( fontSize: 42, )) : widget.messageTheme, diff --git a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart index 5cf53a65..ad3d936a 100644 --- a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart @@ -74,7 +74,7 @@ class QuotedMessageWidget extends StatelessWidget { final Message message; /// The message theme - final MessageTheme messageTheme; + final MessageThemeData messageTheme; /// If true the widget will be mirrored final bool reverse; @@ -139,11 +139,11 @@ class QuotedMessageWidget extends StatelessWidget { message: msg, messageTheme: isOnlyEmoji && _containsText ? messageTheme.copyWith( - messageText: messageTheme.messageText?.copyWith( + messageTextStyle: messageTheme.messageTextStyle?.copyWith( fontSize: 32, )) : messageTheme.copyWith( - messageText: messageTheme.messageText?.copyWith( + messageTextStyle: messageTheme.messageTextStyle?.copyWith( fontSize: 12, )), ), diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index 7a520f40..f82b916c 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -50,8 +50,8 @@ class StreamChatThemeData { ChannelListHeaderTheme? channelListHeaderTheme, ChannelPreviewThemeData? channelPreviewTheme, ChannelHeaderThemeData? channelHeaderTheme, - MessageTheme? otherMessageTheme, - MessageTheme? ownMessageTheme, + MessageThemeData? otherMessageTheme, + MessageThemeData? ownMessageTheme, MessageInputThemeData? messageInputTheme, Widget Function(BuildContext, User)? defaultUserImage, IconThemeData? primaryIconTheme, @@ -159,10 +159,10 @@ class StreamChatThemeData { final GalleryFooterThemeData galleryFooterTheme; /// Theme of the current user messages - final MessageTheme ownMessageTheme; + final MessageThemeData ownMessageTheme; /// Theme of other users messages - final MessageTheme otherMessageTheme; + final MessageThemeData otherMessageTheme; /// Theme dedicated to the [MessageInput] widget final MessageInputThemeData messageInputTheme; @@ -195,8 +195,8 @@ class StreamChatThemeData { ColorTheme? colorTheme, ChannelPreviewThemeData? channelPreviewTheme, ChannelHeaderThemeData? channelHeaderTheme, - MessageTheme? ownMessageTheme, - MessageTheme? otherMessageTheme, + MessageThemeData? ownMessageTheme, + MessageThemeData? otherMessageTheme, MessageInputThemeData? messageInputTheme, Widget Function(BuildContext, User)? defaultUserImage, IconThemeData? primaryIconTheme, @@ -324,13 +324,13 @@ class StreamChatThemeData { title: textTheme.headlineBold, ), channelHeaderTheme: channelHeaderTheme, - ownMessageTheme: MessageTheme( - messageAuthor: + ownMessageTheme: MessageThemeData( + messageAuthorStyle: textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), - messageText: textTheme.body, - createdAt: + messageTextStyle: textTheme.body, + createdAtStyle: textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), - replies: textTheme.footnoteBold.copyWith(color: accentColor), + repliesStyle: textTheme.footnoteBold.copyWith(color: accentColor), messageBackgroundColor: colorTheme.disabled, reactionsBackgroundColor: colorTheme.barsBg, reactionsBorderColor: colorTheme.borders, @@ -343,21 +343,21 @@ class StreamChatThemeData { width: 32, ), ), - messageLinks: TextStyle( + messageLinksStyle: TextStyle( color: accentColor, ), ), - otherMessageTheme: MessageTheme( + otherMessageTheme: MessageThemeData( reactionsBackgroundColor: colorTheme.disabled, reactionsBorderColor: colorTheme.barsBg, reactionsMaskColor: colorTheme.appBg, - messageText: textTheme.body, - createdAt: + messageTextStyle: textTheme.body, + createdAtStyle: textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), - messageAuthor: + messageAuthorStyle: textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), - replies: textTheme.footnoteBold.copyWith(color: accentColor), - messageLinks: TextStyle( + repliesStyle: textTheme.footnoteBold.copyWith(color: accentColor), + messageLinksStyle: TextStyle( color: accentColor, ), messageBackgroundColor: colorTheme.barsBg, diff --git a/packages/stream_chat_flutter/lib/src/theme/message_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_theme.dart index e9deb1f4..023a2917 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_theme.dart @@ -1,37 +1,38 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; /// Class for getting message theme -class MessageTheme { - /// Constructor into [MessageTheme] - const MessageTheme({ - this.replies, - this.messageText, - this.messageAuthor, - this.messageLinks, +class MessageThemeData with Diagnosticable { + /// Creates a [MessageThemeData]. + const MessageThemeData({ + this.repliesStyle, + this.messageTextStyle, + this.messageAuthorStyle, + this.messageLinksStyle, this.messageBackgroundColor, this.messageBorderColor, this.reactionsBackgroundColor, this.reactionsBorderColor, this.reactionsMaskColor, this.avatarTheme, - this.createdAt, + this.createdAtStyle, }); /// Text style for message text - final TextStyle? messageText; + final TextStyle? messageTextStyle; /// Text style for message author - final TextStyle? messageAuthor; + final TextStyle? messageAuthorStyle; /// Text style for message links - final TextStyle? messageLinks; + final TextStyle? messageLinksStyle; /// Text style for created at text - final TextStyle? createdAt; + final TextStyle? createdAtStyle; /// Text style for replies - final TextStyle? replies; + final TextStyle? repliesStyle; /// Color for messageBackgroundColor final Color? messageBackgroundColor; @@ -52,12 +53,12 @@ class MessageTheme { final AvatarThemeData? avatarTheme; /// Copy with a theme - MessageTheme copyWith({ - TextStyle? messageText, - TextStyle? messageAuthor, - TextStyle? messageLinks, - TextStyle? createdAt, - TextStyle? replies, + MessageThemeData copyWith({ + TextStyle? messageTextStyle, + TextStyle? messageAuthorStyle, + TextStyle? messageLinksStyle, + TextStyle? createdAtStyle, + TextStyle? repliesStyle, Color? messageBackgroundColor, Color? messageBorderColor, AvatarThemeData? avatarTheme, @@ -65,33 +66,61 @@ class MessageTheme { Color? reactionsBorderColor, Color? reactionsMaskColor, }) => - MessageTheme( - messageText: messageText ?? this.messageText, - messageAuthor: messageAuthor ?? this.messageAuthor, - messageLinks: messageLinks ?? this.messageLinks, - createdAt: createdAt ?? this.createdAt, + MessageThemeData( + messageTextStyle: messageTextStyle ?? this.messageTextStyle, + messageAuthorStyle: messageAuthorStyle ?? this.messageAuthorStyle, + messageLinksStyle: messageLinksStyle ?? this.messageLinksStyle, + createdAtStyle: createdAtStyle ?? this.createdAtStyle, messageBackgroundColor: messageBackgroundColor ?? this.messageBackgroundColor, messageBorderColor: messageBorderColor ?? this.messageBorderColor, avatarTheme: avatarTheme ?? this.avatarTheme, - replies: replies ?? this.replies, + repliesStyle: repliesStyle ?? this.repliesStyle, reactionsBackgroundColor: reactionsBackgroundColor ?? this.reactionsBackgroundColor, reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor, reactionsMaskColor: reactionsMaskColor ?? this.reactionsMaskColor, ); + /// Linearly interpolate from one [MessageThemeData] to another. + MessageThemeData lerp(MessageThemeData a, MessageThemeData b, double t) => + MessageThemeData( + avatarTheme: + const AvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), + createdAtStyle: TextStyle.lerp(a.createdAtStyle, b.createdAtStyle, t), + messageAuthorStyle: + TextStyle.lerp(a.messageAuthorStyle, b.messageAuthorStyle, t), + messageBackgroundColor: + Color.lerp(a.messageBackgroundColor, b.messageBackgroundColor, t), + messageBorderColor: + Color.lerp(a.messageBorderColor, b.messageBorderColor, t), + messageLinksStyle: + TextStyle.lerp(a.messageLinksStyle, b.messageLinksStyle, t), + messageTextStyle: + TextStyle.lerp(a.messageTextStyle, b.messageTextStyle, t), + reactionsBackgroundColor: Color.lerp( + a.reactionsBackgroundColor, b.reactionsBackgroundColor, t), + reactionsBorderColor: + Color.lerp(a.messageBorderColor, b.reactionsBorderColor, t), + reactionsMaskColor: + Color.lerp(a.reactionsMaskColor, b.reactionsMaskColor, t), + repliesStyle: TextStyle.lerp(a.repliesStyle, b.repliesStyle, t), + ); + /// Merge with a theme - MessageTheme merge(MessageTheme? other) { + MessageThemeData merge(MessageThemeData? 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, + messageTextStyle: messageTextStyle?.merge(other.messageTextStyle) ?? + other.messageTextStyle, + messageAuthorStyle: messageAuthorStyle?.merge(other.messageAuthorStyle) ?? + other.messageAuthorStyle, + messageLinksStyle: messageLinksStyle?.merge(other.messageLinksStyle) ?? + other.messageLinksStyle, + createdAtStyle: + createdAtStyle?.merge(other.createdAtStyle) ?? other.createdAtStyle, + repliesStyle: + repliesStyle?.merge(other.repliesStyle) ?? other.repliesStyle, messageBackgroundColor: other.messageBackgroundColor, messageBorderColor: other.messageBorderColor, avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme, @@ -100,4 +129,52 @@ class MessageTheme { reactionsMaskColor: other.reactionsMaskColor, ); } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MessageThemeData && + runtimeType == other.runtimeType && + messageTextStyle == other.messageTextStyle && + messageAuthorStyle == other.messageAuthorStyle && + messageLinksStyle == other.messageLinksStyle && + createdAtStyle == other.createdAtStyle && + repliesStyle == other.repliesStyle && + messageBackgroundColor == other.messageBackgroundColor && + messageBorderColor == other.messageBorderColor && + reactionsBackgroundColor == other.reactionsBackgroundColor && + reactionsBorderColor == other.reactionsBorderColor && + reactionsMaskColor == other.reactionsMaskColor && + avatarTheme == other.avatarTheme; + + @override + int get hashCode => + messageTextStyle.hashCode ^ + messageAuthorStyle.hashCode ^ + messageLinksStyle.hashCode ^ + createdAtStyle.hashCode ^ + repliesStyle.hashCode ^ + messageBackgroundColor.hashCode ^ + messageBorderColor.hashCode ^ + reactionsBackgroundColor.hashCode ^ + reactionsBorderColor.hashCode ^ + reactionsMaskColor.hashCode ^ + avatarTheme.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('messageTextStyle', messageTextStyle)) + ..add(DiagnosticsProperty('messageAuthorStyle', messageAuthorStyle)) + ..add(DiagnosticsProperty('messageLinksStyle', messageLinksStyle)) + ..add(DiagnosticsProperty('createdAtStyle', createdAtStyle)) + ..add(DiagnosticsProperty('repliesStyle', repliesStyle)) + ..add(ColorProperty('messageBackgroundColor', messageBackgroundColor)) + ..add(ColorProperty('messageBorderColor', messageBorderColor)) + ..add(DiagnosticsProperty('avatarTheme', avatarTheme)) + ..add(ColorProperty('reactionsBackgroundColor', reactionsBackgroundColor)) + ..add(ColorProperty('reactionsBorderColor', reactionsBorderColor)) + ..add(ColorProperty('reactionsMaskColor', reactionsMaskColor)); + } } diff --git a/packages/stream_chat_flutter/test/src/deleted_message_test.dart b/packages/stream_chat_flutter/test/src/deleted_message_test.dart index ac953ee9..0d86e89c 100644 --- a/packages/stream_chat_flutter/test/src/deleted_message_test.dart +++ b/packages/stream_chat_flutter/test/src/deleted_message_test.dart @@ -21,11 +21,11 @@ void main() { client: client, child: const Scaffold( body: DeletedMessage( - messageTheme: MessageTheme( - createdAt: TextStyle( + messageTheme: MessageThemeData( + createdAtStyle: TextStyle( color: Colors.black, ), - messageText: TextStyle(), + messageTextStyle: TextStyle(), ), ), ), From c3d2896071b5392cd99f3976e9784b8a158d6476 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Mon, 2 Aug 2021 10:56:06 -0400 Subject: [PATCH 042/121] test: add tests for MessageTheme --- .../test/src/theme/message_theme_test.dart | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 packages/stream_chat_flutter/test/src/theme/message_theme_test.dart diff --git a/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart new file mode 100644 index 00000000..5908d864 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart' hide TextTheme; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + test('MessageThemeData copyWith, ==, hashCode basics', () { + expect(const MessageThemeData(), const MessageThemeData().copyWith()); + expect(const MessageThemeData().hashCode, + const MessageThemeData().copyWith().hashCode); + }); + + group('MessageThemeData lerps', () { + test('''Light MessageThemeData lerps completely to dark MessageThemeData''', + () { + expect( + const MessageThemeData() + .lerp(_messageThemeControl, _messageThemeControlDark, 1), + _messageThemeControlDark); + }); + + test('''Dark MessageThemeData lerps completely to light MessageThemeData''', + () { + expect( + const MessageThemeData() + .lerp(_messageThemeControlDark, _messageThemeControl, 1), + _messageThemeControl); + }); + }); + + test('Merging dark and light themes results in a dark theme', () { + expect(_messageThemeControl.merge(_messageThemeControlDark), + _messageThemeControlDark); + }); +} + +final _messageThemeControl = MessageThemeData( + messageAuthorStyle: TextTheme.light().footnote.copyWith( + color: ColorTheme.light().textLowEmphasis, + ), + messageTextStyle: TextTheme.light().body, + createdAtStyle: TextTheme.light().footnote.copyWith( + color: ColorTheme.light().textLowEmphasis, + ), + repliesStyle: TextTheme.light().footnoteBold.copyWith( + color: ColorTheme.light().accentPrimary, + ), + messageBackgroundColor: ColorTheme.light().disabled, + reactionsBackgroundColor: ColorTheme.light().barsBg, + reactionsBorderColor: ColorTheme.light().borders, + reactionsMaskColor: ColorTheme.light().appBg, + messageBorderColor: ColorTheme.light().disabled, + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 32, + width: 32, + ), + ), + messageLinksStyle: TextStyle( + color: ColorTheme.light().accentPrimary, + ), +); + +final _messageThemeControlDark = MessageThemeData( + messageAuthorStyle: TextTheme.dark().footnote.copyWith( + color: ColorTheme.dark().textLowEmphasis, + ), + messageTextStyle: TextTheme.dark().body, + createdAtStyle: TextTheme.dark().footnote.copyWith( + color: ColorTheme.dark().textLowEmphasis, + ), + repliesStyle: TextTheme.dark().footnoteBold.copyWith( + color: ColorTheme.dark().accentPrimary, + ), + messageBackgroundColor: ColorTheme.dark().disabled, + reactionsBackgroundColor: ColorTheme.dark().barsBg, + reactionsBorderColor: ColorTheme.dark().borders, + reactionsMaskColor: ColorTheme.dark().appBg, + messageBorderColor: ColorTheme.dark().disabled, + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 32, + width: 32, + ), + ), + messageLinksStyle: TextStyle( + color: ColorTheme.dark().accentPrimary, + ), +); From 51e0eb9a132bce35216dfcd187f6ad4e9703e4e0 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Mon, 2 Aug 2021 11:02:55 -0400 Subject: [PATCH 043/121] chore: use ChannelPreviewTheme.of(context) rather than StreamChatTheme.of(context).channelPreviewTheme --- packages/stream_chat_flutter/lib/src/channel_preview.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index 8a9eb9f1..2929c6f4 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -68,7 +68,7 @@ class ChannelPreview extends StatelessWidget { @override Widget build(BuildContext context) { - final channelPreviewTheme = StreamChatTheme.of(context).channelPreviewTheme; + final channelPreviewTheme = ChannelPreviewTheme.of(context); final streamChatState = StreamChat.of(context); return BetterStreamBuilder( stream: channel.isMutedStream, From 34476db6f44d790f1211196be6f675d4a6aa9d31 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Mon, 2 Aug 2021 11:52:45 -0400 Subject: [PATCH 044/121] chore: update ChannelListHeaderTheme --- .../lib/src/channel_list_header.dart | 22 ++-- .../lib/src/stream_chat_theme.dart | 10 +- .../src/theme/channel_list_header_theme.dart | 108 +++++++++++++++--- 3 files changed, 110 insertions(+), 30 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel_list_header.dart b/packages/stream_chat_flutter/lib/src/channel_list_header.dart index ae017090..72145fe4 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_header.dart @@ -45,7 +45,7 @@ typedef TitleBuilder = Widget Function( /// 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 [ChannelListHeaderTheme] property. +/// [StreamChatTheme] and on its [ChannelListHeaderThemeData] property. /// Modify it to change the widget appearance. class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { /// Instantiates a ChannelListHeader @@ -115,6 +115,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { } final chatThemeData = StreamChatTheme.of(context); + final channelListHeaderThemeData = ChannelListHeaderTheme.of(context); return InfoTile( // ignore: avoid_bool_literals_in_conditional_expressions showMessage: showConnectionStateTile ? showStatus : false, @@ -123,7 +124,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { textTheme: Theme.of(context).textTheme, brightness: Theme.of(context).brightness, elevation: 1, - backgroundColor: chatThemeData.channelListHeaderTheme.color, + backgroundColor: channelListHeaderThemeData.color, centerTitle: true, leading: leading ?? Center( @@ -138,10 +139,10 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { } Scaffold.of(context).openDrawer(); }, - borderRadius: chatThemeData - .channelListHeaderTheme.avatarTheme?.borderRadius, - constraints: chatThemeData - .channelListHeaderTheme.avatarTheme?.constraints, + borderRadius: channelListHeaderThemeData + .avatarTheme?.borderRadius, + constraints: channelListHeaderThemeData + .avatarTheme?.constraints, ) : const Offstage(), ), @@ -227,10 +228,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { const SizedBox(width: 10), Text( 'Searching for Network', - style: StreamChatTheme.of(context) - .channelListHeaderTheme - .title - ?.copyWith( + style: ChannelListHeaderTheme.of(context).titleStyle?.copyWith( fontSize: 16, fontWeight: FontWeight.bold, ), @@ -248,7 +246,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { children: [ Text( 'Offline...', - style: chatThemeData.channelListHeaderTheme.title?.copyWith( + style: chatThemeData.channelListHeaderTheme.titleStyle?.copyWith( fontSize: 16, fontWeight: FontWeight.bold, ), @@ -259,7 +257,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { ..openConnection(), child: Text( 'Try Again', - style: chatThemeData.channelListHeaderTheme.title?.copyWith( + style: chatThemeData.channelListHeaderTheme.titleStyle?.copyWith( fontSize: 16, fontWeight: FontWeight.bold, color: chatThemeData.colorTheme.accentPrimary, diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index f82b916c..5fe56dbc 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -47,7 +47,7 @@ class StreamChatThemeData { Brightness? brightness, TextTheme? textTheme, ColorTheme? colorTheme, - ChannelListHeaderTheme? channelListHeaderTheme, + ChannelListHeaderThemeData? channelListHeaderTheme, ChannelPreviewThemeData? channelPreviewTheme, ChannelHeaderThemeData? channelHeaderTheme, MessageThemeData? otherMessageTheme, @@ -145,7 +145,7 @@ class StreamChatThemeData { final ChannelPreviewThemeData channelPreviewTheme; /// Theme of the [ChannelListHeader] - final ChannelListHeaderTheme channelListHeaderTheme; + final ChannelListHeaderThemeData channelListHeaderTheme; /// Theme of the chat widgets dedicated to a channel header final ChannelHeaderThemeData channelHeaderTheme; @@ -200,7 +200,7 @@ class StreamChatThemeData { MessageInputThemeData? messageInputTheme, Widget Function(BuildContext, User)? defaultUserImage, IconThemeData? primaryIconTheme, - ChannelListHeaderTheme? channelListHeaderTheme, + ChannelListHeaderThemeData? channelListHeaderTheme, List? reactionIcons, GalleryHeaderThemeData? galleryHeaderTheme, GalleryFooterThemeData? galleryFooterTheme, @@ -312,7 +312,7 @@ class StreamChatThemeData { ), ), channelPreviewTheme: channelPreviewTheme, - channelListHeaderTheme: ChannelListHeaderTheme( + channelListHeaderTheme: ChannelListHeaderThemeData( avatarTheme: AvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( @@ -321,7 +321,7 @@ class StreamChatThemeData { ), ), color: colorTheme.barsBg, - title: textTheme.headlineBold, + titleStyle: textTheme.headlineBold, ), channelHeaderTheme: channelHeaderTheme, ownMessageTheme: MessageThemeData( diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart index 339361b9..3846232e 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart @@ -1,17 +1,63 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; -/// Theme dedicated to the [ChannelListHeader] -class ChannelListHeaderTheme { - /// Returns a new [ChannelListHeaderTheme] +/// Overrides the default style of [ChannelListHeader] descendants. +/// +/// See also: +/// +/// * [ChannelListHeaderThemeData], which is used to configure this theme. +class ChannelListHeaderTheme extends InheritedTheme { + /// Creates a [ChannelListHeaderTheme]. + /// + /// The [data] parameter must not be null. const ChannelListHeaderTheme({ - this.title, + Key? key, + required this.data, + required Widget child, + }) : super(key: key, child: child); + + /// The configuration of this theme. + final ChannelListHeaderThemeData data; + + /// The closest instance of this class that encloses the given context. + /// + /// If there is no enclosing [ChannelListHeaderTheme] widget, then + /// [StreamChatThemeData.channelListHeaderTheme] is used. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// final theme = ChannelListHeaderTheme.of(context); + /// ``` + static ChannelListHeaderThemeData of(BuildContext context) { + final channelListHeaderTheme = + context.dependOnInheritedWidgetOfExactType(); + return channelListHeaderTheme?.data ?? + StreamChatTheme.of(context).channelListHeaderTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) => + ChannelListHeaderTheme(data: data, child: child); + + @override + bool updateShouldNotify(ChannelListHeaderTheme oldWidget) => + data != oldWidget.data; +} + +/// Theme dedicated to the [ChannelListHeader] +class ChannelListHeaderThemeData with Diagnosticable { + /// Returns a new [ChannelListHeaderThemeData] + const ChannelListHeaderThemeData({ + this.titleStyle, this.avatarTheme, this.color, }); /// Style of the title text - final TextStyle? title; + final TextStyle? titleStyle; /// Theme dedicated to the userAvatar final AvatarThemeData? avatarTheme; @@ -19,25 +65,61 @@ class ChannelListHeaderTheme { /// Background color of the appbar final Color? color; - /// Returns a new [ChannelListHeaderTheme] replacing some of its properties - ChannelListHeaderTheme copyWith({ - TextStyle? title, + /// Returns a new [ChannelListHeaderThemeData] replacing some of its + /// properties + ChannelListHeaderThemeData copyWith({ + TextStyle? titleStyle, AvatarThemeData? avatarTheme, Color? color, }) => - ChannelListHeaderTheme( - title: title ?? this.title, + ChannelListHeaderThemeData( + titleStyle: titleStyle ?? this.titleStyle, avatarTheme: avatarTheme ?? this.avatarTheme, color: color ?? this.color, ); - /// Merges [this] [ChannelListHeaderTheme] with the [other] - ChannelListHeaderTheme merge(ChannelListHeaderTheme? other) { + /// Linearly interpolate from one [ChannelListHeaderThemeData] to another. + ChannelListHeaderThemeData lerp( + ChannelListHeaderThemeData a, + ChannelListHeaderThemeData b, + double t, + ) => + ChannelListHeaderThemeData( + avatarTheme: + const AvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), + color: Color.lerp(a.color, b.color, t), + titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t), + ); + + /// Merges [this] [ChannelListHeaderThemeData] with the [other] + ChannelListHeaderThemeData merge(ChannelListHeaderThemeData? other) { if (other == null) return this; return copyWith( - title: title?.merge(other.title) ?? other.title, + titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle, avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme, color: other.color, ); } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ChannelListHeaderThemeData && + runtimeType == other.runtimeType && + titleStyle == other.titleStyle && + avatarTheme == other.avatarTheme && + color == other.color; + + @override + int get hashCode => + titleStyle.hashCode ^ avatarTheme.hashCode ^ color.hashCode; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('titleStyle', titleStyle)) + ..add(DiagnosticsProperty('avatarTheme', avatarTheme)) + ..add(ColorProperty('color', color)); + } } From 26b2bf3f18db0ac819c346eba211c8c7baf810a8 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Mon, 2 Aug 2021 12:02:18 -0400 Subject: [PATCH 045/121] test: add test for ChannelListHeaderTheme --- .../theme/channel_list_header_theme_test.dart | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 packages/stream_chat_flutter/test/src/theme/channel_list_header_theme_test.dart diff --git a/packages/stream_chat_flutter/test/src/theme/channel_list_header_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/channel_list_header_theme_test.dart new file mode 100644 index 00000000..aa378f3a --- /dev/null +++ b/packages/stream_chat_flutter/test/src/theme/channel_list_header_theme_test.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart' hide TextTheme; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + test('ChannelListHeaderThemeData copyWith, ==, hashCode basics', () { + expect(const ChannelListHeaderThemeData(), + const ChannelListHeaderThemeData().copyWith()); + expect(const ChannelListHeaderThemeData().hashCode, + const ChannelListHeaderThemeData().copyWith().hashCode); + }); + + group('ChannelListHeaderThemeData lerps', () { + test( + '''Light ChannelListHeaderThemeData lerps completely to dark ChannelListHeaderThemeData''', + () { + expect( + const ChannelListHeaderThemeData().lerp( + _channelListHeaderThemeControl, + _channelListHeaderThemeControlDark, + 1), + _channelListHeaderThemeControlDark); + }); + + test( + '''Light ChannelListHeaderThemeData lerps halfway to dark ChannelListHeaderThemeData''', + () { + expect( + const ChannelListHeaderThemeData().lerp( + _channelListHeaderThemeControl, + _channelListHeaderThemeControlDark, + 0.5), + _channelListHeaderThemeControlMidLerp); + }); + + test( + '''Dark ChannelListHeaderThemeData lerps completely to light ChannelListHeaderThemeData''', + () { + expect( + const ChannelListHeaderThemeData().lerp( + _channelListHeaderThemeControlDark, + _channelListHeaderThemeControl, + 1), + _channelListHeaderThemeControl); + }); + }); + + test('Merging dark and light themes results in a dark theme', () { + expect( + _channelListHeaderThemeControl + .merge(_channelListHeaderThemeControlDark), + _channelListHeaderThemeControlDark); + }); +} + +final _channelListHeaderThemeControl = ChannelListHeaderThemeData( + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + color: ColorTheme.light().barsBg, + titleStyle: TextTheme.light().headlineBold, +); + +final _channelListHeaderThemeControlMidLerp = ChannelListHeaderThemeData( + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + color: const Color(0xff87898b), + titleStyle: const TextStyle( + color: Color(0xff7f7f7f), + fontSize: 16, + fontWeight: FontWeight.bold, + ), +); + +final _channelListHeaderThemeControlDark = ChannelListHeaderThemeData( + avatarTheme: AvatarThemeData( + borderRadius: BorderRadius.circular(20), + constraints: const BoxConstraints.tightFor( + height: 40, + width: 40, + ), + ), + color: ColorTheme.dark().barsBg, + titleStyle: TextTheme.dark().headlineBold, +); From d5f38fa54561c99f43ee58f94c9c57b6c8f81c10 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Mon, 2 Aug 2021 12:58:31 -0400 Subject: [PATCH 046/121] test: fixed failing test in channel_preview_theme_test.dart --- .../test/src/theme/channel_preview_theme_test.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat_flutter/test/src/theme/channel_preview_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/channel_preview_theme_test.dart index 24ad716b..3be39952 100644 --- a/packages/stream_chat_flutter/test/src/theme/channel_preview_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/channel_preview_theme_test.dart @@ -65,7 +65,7 @@ final _channelPreviewThemeControl = ChannelPreviewThemeData( ); final _channelPreviewThemeControlMidLerp = ChannelPreviewThemeData( - unreadCounterColor: const Color(0xffff3842), + unreadCounterColor: const Color(0xffff3742), avatarTheme: AvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( @@ -74,7 +74,7 @@ final _channelPreviewThemeControlMidLerp = ChannelPreviewThemeData( ), ), titleStyle: const TextStyle( - color: Color(0xff000000), + color: Color(0xff7f7f7f), fontSize: 14, fontWeight: FontWeight.bold, ), @@ -83,7 +83,7 @@ final _channelPreviewThemeControlMidLerp = ChannelPreviewThemeData( fontSize: 12, ), lastMessageAtStyle: TextTheme.light().footnote.copyWith( - color: Color(0x80000000).withOpacity(.5), + color: const Color(0x807f7f7f).withOpacity(.5), ), indicatorIconSize: 16, ); From 6e6ce8c7f3d57ed562de503e3865678185be6934 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Mon, 2 Aug 2021 14:44:36 -0400 Subject: [PATCH 047/121] test: fix mid-lerp test in channel_header_theme_test.dart --- .../test/src/theme/channel_header_theme_test.dart | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat_flutter/test/src/theme/channel_header_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/channel_header_theme_test.dart index de9803b7..475b1084 100644 --- a/packages/stream_chat_flutter/test/src/theme/channel_header_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/channel_header_theme_test.dart @@ -26,7 +26,7 @@ void main() { expect( const ChannelHeaderThemeData() .lerp(_channelThemeControl, _channelThemeControlDark, 0.5), - _channelThemeControlDark); + _channelThemeControlMidLerp); }); test( @@ -70,10 +70,14 @@ final _channelThemeControlMidLerp = ChannelHeaderThemeData( width: 40, ), ), - color: ColorTheme.light().barsBg, - titleStyle: TextTheme.light().headlineBold, + color: const Color(0xff101418), + titleStyle: const TextStyle( + color: Color(0xffffffff), + fontWeight: FontWeight.bold, + fontSize: 16, + ), subtitleStyle: TextTheme.light().footnote.copyWith( - color: const Color(0xff7A7A7A), + color: const Color(0xff7a7a7a), ), ); From 7aafc405f133b9dd18199bd32a5c11efce3b3ab1 Mon Sep 17 00:00:00 2001 From: Tom Beckett Date: Tue, 3 Aug 2021 14:26:03 +0100 Subject: [PATCH 048/121] fix: Add missing forward slash to markAllRead url # Submit a pull request ## CLA - [x] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required). - [x] The code changes follow best practices - [ ] Code changes are tested (add some information if not applicable) ## Description of the pull request Running latest GetStream SDK (from version 2.0.0) calling `channel.markAllRead()` will throw an error similar to this: ```bash I/flutter (10572): ----------------FIREBASE CRASHLYTICS---------------- I/flutter (10572): StreamChatNetworkError(code: -1, message: SocketException: Failed host lookup: 'chat-us-east-1.stream-io-api.comchannels' (OS Error: No address associated with hostname, errno = 7)) I/flutter (10572): #0 StreamHttpClient.post (package:stream_chat/src/core/http/stream_http_client.dart:158:7) I/flutter (10572): I/flutter (10572): #1 ChannelApi.markAllRead (package:stream_chat/src/core/api/channel_api.dart:87:22) I/flutter (10572): I/flutter (10572): ---------------------------------------------------- ``` Adding a `/` to the front of the called URL will resolve this - I've done this locally and it works. --- packages/stream_chat/lib/src/core/api/channel_api.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat/lib/src/core/api/channel_api.dart b/packages/stream_chat/lib/src/core/api/channel_api.dart index 1ba7f333..c04f9683 100644 --- a/packages/stream_chat/lib/src/core/api/channel_api.dart +++ b/packages/stream_chat/lib/src/core/api/channel_api.dart @@ -84,7 +84,7 @@ class ChannelApi { /// Mark all channels for this user as read Future markAllRead() async { - final response = await _client.post('channels/read'); + final response = await _client.post('/channels/read'); return EmptyResponse.fromJson(response.data); } From 8ba82e8931bb494c4a72f8d796d074ec57784064 Mon Sep 17 00:00:00 2001 From: Tom Beckett Date: Tue, 3 Aug 2021 14:40:16 +0100 Subject: [PATCH 049/121] chore(llc): add changelog entry --- packages/stream_chat/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 9bfe494f..df4ad821 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -3,6 +3,7 @@ 🐞 Fixed - Mutes were not working correctly in 2.1.0 +- Fixed `channel.markAllRead` throwing failed host lookup. ## 2.1.0 From dd5b9bb256fae0fbe2e007ef284e174564165e1e Mon Sep 17 00:00:00 2001 From: Tom Beckett Date: Tue, 3 Aug 2021 14:40:53 +0100 Subject: [PATCH 050/121] fix(llc): channel.markAllRead --- packages/stream_chat/test/src/core/api/channel_api_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat/test/src/core/api/channel_api_test.dart b/packages/stream_chat/test/src/core/api/channel_api_test.dart index c43f161f..8ffd778d 100644 --- a/packages/stream_chat/test/src/core/api/channel_api_test.dart +++ b/packages/stream_chat/test/src/core/api/channel_api_test.dart @@ -176,7 +176,7 @@ void main() { }); test('markAllRead', () async { - const path = 'channels/read'; + const path = '/channels/read'; when(() => client.post(path)).thenAnswer( (_) async => successResponse(path, data: {})); From 5f10220bce5ce3d44c1d2661e29428712be765fd Mon Sep 17 00:00:00 2001 From: Tom Beckett Date: Tue, 3 Aug 2021 14:52:41 +0100 Subject: [PATCH 051/121] chore(llc): Moved entry to correct heading. --- packages/stream_chat/CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index df4ad821..d7896042 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -2,8 +2,13 @@ 🐞 Fixed +- Fixed `channel.markAllRead` throwing failed host lookup. + +## 2.1.1 + +🐞 Fixed + - Mutes were not working correctly in 2.1.0 -- Fixed `channel.markAllRead` throwing failed host lookup. ## 2.1.0 From 558845aabcb01f8956a9f8cae2b988f71e25db26 Mon Sep 17 00:00:00 2001 From: Tom Beckett Date: Tue, 3 Aug 2021 14:53:19 +0100 Subject: [PATCH 052/121] chore(llc): version changelog bymp --- packages/stream_chat/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index d7896042..5f616d83 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,4 +1,4 @@ -## 2.1.1 +## 2.1.2 🐞 Fixed From df0a59fd5a888120e4142490eeb79bc9784600d3 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 3 Aug 2021 19:30:57 +0530 Subject: [PATCH 053/121] Update packages/stream_chat/CHANGELOG.md --- packages/stream_chat/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 5f616d83..d8860425 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,4 +1,4 @@ -## 2.1.2 +## Upcoming 🐞 Fixed From acd932ccc9dedc63ed0f6d3f7031ad935d6452c7 Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Wed, 4 Aug 2021 16:42:01 +0200 Subject: [PATCH 054/121] feat: add image property to user --- .../lib/src/core/models/own_user.dart | 38 ++++--- .../lib/src/core/models/own_user.g.dart | 1 + .../stream_chat/lib/src/core/models/user.dart | 104 ++++++++++++++---- .../lib/src/core/models/user.g.dart | 4 +- .../lib/src/user_avatar.dart | 7 +- 5 files changed, 112 insertions(+), 42 deletions(-) diff --git a/packages/stream_chat/lib/src/core/models/own_user.dart b/packages/stream_chat/lib/src/core/models/own_user.dart index fb5b1caf..5adc753c 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.dart @@ -28,6 +28,7 @@ class OwnUser extends User { bool banned = false, List teams = const [], String? language, + String? image, }) : super( id: id, role: role, @@ -39,6 +40,7 @@ class OwnUser extends User { banned: banned, teams: teams, language: language, + image: image, ); /// Create a new instance from a json @@ -57,6 +59,7 @@ class OwnUser extends User { extraData: user.extraData, teams: user.teams, language: user.language, + image: user.image, ); /// Creates a copy of [OwnUser] with specified attributes overridden. @@ -77,24 +80,26 @@ class OwnUser extends User { int? totalUnreadCount, int? unreadChannels, String? language, + String? image, }) => OwnUser( - id: id ?? this.id, - banned: banned ?? this.banned, - role: role ?? this.role, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - lastActive: lastActive ?? this.lastActive, - online: online ?? this.online, - extraData: extraData ?? this.extraData, - teams: teams ?? this.teams, - channelMutes: channelMutes ?? this.channelMutes, - devices: devices ?? this.devices, - mutes: mutes ?? this.mutes, - totalUnreadCount: totalUnreadCount ?? this.totalUnreadCount, - unreadChannels: unreadChannels ?? this.unreadChannels, - language: language ?? this.language, - ); + id: id ?? this.id, + banned: banned ?? this.banned, + role: role ?? this.role, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + lastActive: lastActive ?? this.lastActive, + online: online ?? this.online, + extraData: extraData ?? this.extraData, + teams: teams ?? this.teams, + channelMutes: channelMutes ?? this.channelMutes, + devices: devices ?? this.devices, + mutes: mutes ?? this.mutes, + totalUnreadCount: totalUnreadCount ?? this.totalUnreadCount, + unreadChannels: unreadChannels ?? this.unreadChannels, + language: language ?? this.language, + image: image // if null, it will be retrieved from extraData['image'] + ); /// Returns a new [OwnUser] that is a combination of this ownUser /// and the given [other] ownUser. @@ -116,6 +121,7 @@ class OwnUser extends User { unreadChannels: other.unreadChannels, updatedAt: other.updatedAt, language: other.language, + image: other.image, ); } diff --git a/packages/stream_chat/lib/src/core/models/own_user.g.dart b/packages/stream_chat/lib/src/core/models/own_user.g.dart index ca4acdea..cacb99c5 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.g.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.g.dart @@ -40,5 +40,6 @@ OwnUser _$OwnUserFromJson(Map json) { (json['teams'] as List?)?.map((e) => e as String).toList() ?? [], language: json['language'] as String?, + image: json['image'] as String?, ); } diff --git a/packages/stream_chat/lib/src/core/models/user.dart b/packages/stream_chat/lib/src/core/models/user.dart index a7e7a51e..9baaf049 100644 --- a/packages/stream_chat/lib/src/core/models/user.dart +++ b/packages/stream_chat/lib/src/core/models/user.dart @@ -4,29 +4,45 @@ import 'package:stream_chat/src/core/util/serializer.dart'; part 'user.g.dart'; -/// The class that defines the user model +/// Class that defines a Stream Chat User. @JsonSerializable() class User extends Equatable { - /// Constructor used for json serialization + /// Creates a new user. + /// + /// If an [image] is provided it will be set on [extraData] with a `key` + /// of 'image'. + /// + /// For example: + /// ```dart + /// final user = User(id: 'id', image: 'https://getstream.io/image.png'); + /// print(user.image == user.extraData['image']); // true + /// ``` User({ required this.id, this.role, + String? image, DateTime? createdAt, DateTime? updatedAt, this.lastActive, + Map extraData = const {}, this.online = false, - this.extraData = const {}, this.banned = false, this.teams = const [], this.language, - }) : createdAt = createdAt ?? DateTime.now(), - updatedAt = updatedAt ?? DateTime.now(); + }) : _image = image, + createdAt = createdAt ?? DateTime.now(), + updatedAt = updatedAt ?? DateTime.now(), - /// Create a new instance from a json + // For backwards compatibalitity, set 'image' on [extraData]. + extraData = + (image != null) ? {...extraData, 'image': image} : extraData; + + /// Create a new instance from json. factory User.fromJson(Map json) => _$UserFromJson(Serializer.moveToExtraDataFromRoot(json, topLevelFields)); /// Known top level fields. + /// /// Useful for [Serializer] methods. static const topLevelFields = [ 'id', @@ -38,16 +54,64 @@ class User extends Equatable { 'banned', 'teams', 'language', + 'image', ]; - /// User id + /// User id. final String id; - /// User role + /// User role. @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final String? role; - /// User role + /// Image for user. This is also set on `extraData['image']`. + /// + /// {@template image} + /// There are a few ways to set an image. + /// + /// Setting an image by passing in an image argument: + /// ```dart + /// final user = User( + /// id: 'id', + /// image: 'https://getstream.io/image', + /// ); + /// ``` + /// + /// Or by directly setting it in [extraData], for example: + /// ```dart + /// final user = User( + /// id: 'id', + /// extraData: const {'image': 'https://getstream.io/image'}, + /// ); + /// + /// ``` + /// Parsing json with an 'image' key will automatically set the `image` + /// property and `extraData['image']` key/value. + /// + /// ```dart + /// final user = User.fromJson({ + /// id: 'id', + /// image: 'https://getstream.io/image', // key: image + /// }); + /// + /// print(user.image == user.extraData['image']); // true + /// ``` + /// {@endtemplate} + final String? _image; + + /// Shortcut for user image. + /// + /// {@macro image} + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) + String? get image { + if (_image != null) { + return _image; + } else { + return extraData['image'] as String?; + } + } + + /// User teams @JsonKey( includeIfNull: false, toJson: Serializer.readOnly, @@ -55,29 +119,29 @@ class User extends Equatable { ) final List teams; - /// Date of user creation + /// Date of user creation. @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final DateTime createdAt; - /// Date of last user update + /// Date of last user update. @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final DateTime updatedAt; - /// Date of last user connection + /// Date of last user connection. @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final DateTime? lastActive; - /// True if user is online + /// True if user is online. @JsonKey( includeIfNull: false, toJson: Serializer.readOnly, defaultValue: false) final bool online; - /// True if user is banned from the chat + /// True if user is banned from the chat. @JsonKey( includeIfNull: false, toJson: Serializer.readOnly, defaultValue: false) final bool banned; - /// Map of custom user extraData + /// Map of custom user extraData. @JsonKey( includeIfNull: false, defaultValue: {}, @@ -85,12 +149,10 @@ class User extends Equatable { final Map extraData; /// The language this user prefers. - /// - /// Defaults to 'en'. @JsonKey(includeIfNull: false) final String? language; - /// Shortcut for user name + /// Shortcut for user name. String get name { if (extraData.containsKey('name')) { final name = extraData['name']! as String; @@ -99,11 +161,11 @@ class User extends Equatable { return id; } - /// List of users to list of userIds + /// List of users to list of userIds. static List? toIds(List? users) => users?.map((u) => u.id).toList(); - /// Serialize to json + /// Serialize to json. Map toJson() => Serializer.moveFromExtraDataToRoot( _$UserToJson(this), ); @@ -120,6 +182,7 @@ class User extends Equatable { bool? banned, List? teams, String? language, + String? image, }) => User( id: id ?? this.id, @@ -132,6 +195,7 @@ class User extends Equatable { banned: banned ?? this.banned, teams: teams ?? this.teams, language: language ?? this.language, + image: image, // if null, it will be retrieved from extraData['image'] ); @override diff --git a/packages/stream_chat/lib/src/core/models/user.g.dart b/packages/stream_chat/lib/src/core/models/user.g.dart index dd3183d5..c685cd57 100644 --- a/packages/stream_chat/lib/src/core/models/user.g.dart +++ b/packages/stream_chat/lib/src/core/models/user.g.dart @@ -10,6 +10,7 @@ User _$UserFromJson(Map json) { return User( id: json['id'] as String, role: json['role'] as String?, + image: json['image'] as String?, createdAt: json['created_at'] == null ? null : DateTime.parse(json['created_at'] as String), @@ -19,8 +20,8 @@ User _$UserFromJson(Map json) { lastActive: json['last_active'] == null ? null : DateTime.parse(json['last_active'] as String), - online: json['online'] as bool? ?? false, extraData: json['extra_data'] as Map? ?? {}, + online: json['online'] as bool? ?? false, banned: json['banned'] as bool? ?? false, teams: (json['teams'] as List?)?.map((e) => e as String).toList() ?? @@ -41,6 +42,7 @@ Map _$UserToJson(User instance) { } writeNotNull('role', readonly(instance.role)); + writeNotNull('image', readonly(instance.image)); writeNotNull('teams', readonly(instance.teams)); writeNotNull('created_at', readonly(instance.createdAt)); writeNotNull('updated_at', readonly(instance.updatedAt)); diff --git a/packages/stream_chat_flutter/lib/src/user_avatar.dart b/packages/stream_chat_flutter/lib/src/user_avatar.dart index 31e07f0a..0c897084 100644 --- a/packages/stream_chat_flutter/lib/src/user_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/user_avatar.dart @@ -56,9 +56,7 @@ class UserAvatar extends StatelessWidget { @override Widget build(BuildContext context) { - final hasImage = user.extraData.containsKey('image') && - user.extraData['image'] != null && - user.extraData['image'] != ''; + final hasImage = user.image != null && user.image!.isNotEmpty; final streamChatTheme = StreamChatTheme.of(context); Widget avatar = FittedBox( @@ -75,8 +73,7 @@ class UserAvatar extends StatelessWidget { child: hasImage ? CachedNetworkImage( filterQuality: FilterQuality.high, - // ignore: cast_nullable_to_non_nullable - imageUrl: user.extraData['image'] as String, + imageUrl: user.image!, errorWidget: (_, __, ___) => streamChatTheme.defaultUserImage(context, user), fit: BoxFit.cover, From 27893e147a7e10c69be895f57e02883fdd8f8731 Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Wed, 4 Aug 2021 16:43:45 +0200 Subject: [PATCH 055/121] test: add additional user tests --- packages/stream_chat/test/fixtures/user.json | 11 ++ .../test/src/core/models/own_user_test.dart | 112 ++++++++++++- .../test/src/core/models/user_test.dart | 153 +++++++++++++++++- 3 files changed, 263 insertions(+), 13 deletions(-) diff --git a/packages/stream_chat/test/fixtures/user.json b/packages/stream_chat/test/fixtures/user.json index b22c8553..146a4242 100644 --- a/packages/stream_chat/test/fixtures/user.json +++ b/packages/stream_chat/test/fixtures/user.json @@ -2,5 +2,16 @@ "id": "bbb19d9a-ee50-45bc-84e5-0584e79d0c9e", "role": "test-role", "name": "John", + "image": "https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow", + "extraDataStringTest": "Extra data test", + "extraDataIntTest": 1, + "extraDataDoubleTest": 1.1, + "extraDataBoolTest": true, + "banned": true, + "online": true, + "teams": ["team-1", "team-2"], + "created_at": "2021-08-03 12:39:21.817646", + "updated_at": "2021-08-04 12:39:21.817646", + "last_active" : "2021-08-05 12:39:21.817646", "language": "en" } \ No newline at end of file diff --git a/packages/stream_chat/test/src/core/models/own_user_test.dart b/packages/stream_chat/test/src/core/models/own_user_test.dart index 7aef859d..e1d12ada 100644 --- a/packages/stream_chat/test/src/core/models/own_user_test.dart +++ b/packages/stream_chat/test/src/core/models/own_user_test.dart @@ -1,10 +1,23 @@ +import 'package:mocktail/mocktail.dart'; import 'package:stream_chat/src/core/models/own_user.dart'; import 'package:stream_chat/src/core/models/user.dart'; +import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; import '../../utils.dart'; +class MockMute extends Mock implements Mute {} + +class MockDevice extends Mock implements Device {} + void main() { + final devices = [MockDevice(), MockDevice()]; + final mutes = [MockMute(), MockMute()]; + final channelMutes = [MockMute()]; + final createdAt = DateTime.parse('2021-05-03 12:39:21.817646'); + final updatedAt = DateTime.parse('2021-04-03 12:39:21.817646'); + final lastActive = DateTime.parse('2021-03-03 12:39:21.817646'); + group('src/models/own_user', () { test('should parse json correctly', () { final ownUser = OwnUser.fromJson(jsonFixture('own_user.json')); @@ -22,6 +35,7 @@ void main() { expect(ownUser.channelMutes.length, 1); expect(ownUser.totalUnreadCount, 0); expect(ownUser.unreadChannels, 0); + expect(ownUser.image, 'https://placehold.jp/150x150.png'); expect(ownUser.extraData['image'], 'https://placehold.jp/150x150.png'); expect(ownUser.extraData['name'], 'Proud darkness'); expect(ownUser.extraData['username'], 'Rioland'); @@ -40,6 +54,7 @@ void main() { expect(ownUser.online, user.online); expect(ownUser.banned, user.banned); expect(ownUser.extraData, user.extraData); + expect(ownUser.image, user.image); }); test('copyWith', () { @@ -49,35 +64,116 @@ void main() { expect(newUser.id, user.id); expect(newUser.role, user.role); expect(newUser.name, user.name); + expect(newUser.devices, user.devices); + expect(newUser.mutes, user.mutes); + expect(newUser.totalUnreadCount, user.totalUnreadCount); + expect(newUser.channelMutes, user.channelMutes); + expect(newUser.createdAt, user.createdAt); + expect(newUser.updatedAt, user.updatedAt); + expect(newUser.lastActive, user.lastActive); + expect(newUser.online, user.online); + expect(newUser.extraData, user.extraData); + expect(newUser.banned, user.banned); + expect(newUser.teams, user.teams); + expect(newUser.language, user.language); + expect(newUser.image, user.image); newUser = user.copyWith( id: 'test', role: 'test', + image: 'https://getstream.io/image-new', extraData: { 'name': 'test', }, + devices: devices, + mutes: mutes, + totalUnreadCount: 10, + unreadChannels: 5, + channelMutes: channelMutes, + createdAt: createdAt, + updatedAt: updatedAt, + lastActive: lastActive, + online: true, + banned: true, + teams: ['team1', 'team2'], + language: 'fr', ); expect(newUser.id, 'test'); expect(newUser.role, 'test'); expect(newUser.name, 'test'); + expect(newUser.image, 'https://getstream.io/image-new'); + expect( + newUser.extraData, + { + 'name': 'test', + 'image': 'https://getstream.io/image-new', + }, + reason: 'Should get image from user.image', + ); + expect(newUser.devices, devices); + expect(newUser.mutes, mutes); + expect(newUser.totalUnreadCount, 10); + expect(newUser.unreadChannels, 5); + expect(newUser.channelMutes, channelMutes); + expect(newUser.createdAt, createdAt); + expect(newUser.updatedAt, updatedAt); + expect(newUser.createdAt, createdAt); + expect(newUser.online, true); + expect(newUser.banned, true); + expect(newUser.teams, ['team1', 'team2']); + expect(newUser.language, 'fr'); }); test('merge', () { final user = OwnUser.fromJson(jsonFixture('own_user.json')); - final newUser = user.merge(OwnUser( - id: 'test', - role: 'test', - extraData: const { - 'name': 'test', - }, - banned: true, - )); + final newUser = user.merge( + OwnUser( + id: 'test', + role: 'test', + extraData: const { + 'name': 'test', + }, + image: 'https://getstream.io/image-new', + devices: devices, + mutes: mutes, + totalUnreadCount: 10, + unreadChannels: 5, + channelMutes: channelMutes, + createdAt: createdAt, + updatedAt: updatedAt, + lastActive: lastActive, + online: true, + banned: true, + teams: const ['team1', 'team2'], + language: 'fr', + ), + ); expect(newUser.id, 'test'); expect(newUser.role, 'test'); expect(newUser.name, 'test'); + expect(newUser.image, 'https://getstream.io/image-new'); + expect( + newUser.extraData, + { + 'name': 'test', + 'image': 'https://getstream.io/image-new', + }, + reason: 'Should get image from user.image', + ); + expect(newUser.devices, devices); + expect(newUser.mutes, mutes); + expect(newUser.totalUnreadCount, 10); + expect(newUser.unreadChannels, 5); + expect(newUser.channelMutes, channelMutes); + expect(newUser.createdAt, createdAt); + expect(newUser.updatedAt, updatedAt); + expect(newUser.createdAt, createdAt); + expect(newUser.online, true); expect(newUser.banned, true); + expect(newUser.teams, ['team1', 'team2']); + expect(newUser.language, 'fr'); }); }); } diff --git a/packages/stream_chat/test/src/core/models/user_test.dart b/packages/stream_chat/test/src/core/models/user_test.dart index ce488cab..48306017 100644 --- a/packages/stream_chat/test/src/core/models/user_test.dart +++ b/packages/stream_chat/test/src/core/models/user_test.dart @@ -4,21 +4,73 @@ import 'package:test/test.dart'; import '../../utils.dart'; void main() { + const id = 'bbb19d9a-ee50-45bc-84e5-0584e79d0c9e'; + const role = 'test-role'; + const name = 'John'; + const image = + 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow'; + const extraDataStringTest = 'Extra data test'; + const extraDataIntTest = 1; + const extraDataDoubleTest = 1.1; + const extraDataBoolTest = true; + const online = true; + const banned = true; + const teams = ['team-1', 'team-2']; + const createdAtString = '2021-08-03 12:39:21.817646'; + const updatedAtString = '2021-08-04 12:39:21.817646'; + const lastActiveString = '2021-08-05 12:39:21.817646'; + group('src/models/user', () { test('should parse json correctly', () { final user = User.fromJson(jsonFixture('user.json')); - expect(user.id, 'bbb19d9a-ee50-45bc-84e5-0584e79d0c9e'); - expect(user.name, 'John'); + expect(user.id, id); + expect(user.role, role); + expect(user.name, name); + expect(user.image, image); + expect(user.extraData['image'], image); + expect(user.extraData['extraDataStringTest'], extraDataStringTest); + expect(user.extraData['extraDataIntTest'], extraDataIntTest); + expect(user.extraData['extraDataDoubleTest'], extraDataDoubleTest); + expect(user.extraData['extraDataBoolTest'], extraDataBoolTest); + expect(user.online, online); + expect(user.banned, banned); + expect(user.teams, teams); + expect(user.createdAt, DateTime.parse(createdAtString)); + expect(user.updatedAt, DateTime.parse(updatedAtString)); + expect(user.lastActive, DateTime.parse(lastActiveString)); + expect(user.language, 'en'); }); test('should serialize to json correctly', () { final user = User( - id: 'bbb19d9a-ee50-45bc-84e5-0584e79d0c9e', - role: 'abc', + id: id, + role: role, + image: image, + extraData: const { + 'name': name, + 'extraDataStringTest': extraDataStringTest, + 'extraDataIntTest': extraDataIntTest, + 'extraDataDoubleTest': extraDataDoubleTest, + 'extraDataBoolTest': extraDataBoolTest, + }, + createdAt: DateTime.parse(createdAtString), + updatedAt: DateTime.parse(updatedAtString), + lastActive: DateTime.parse(lastActiveString), + banned: online, + online: banned, + teams: const ['team-1', 'team-2'], + language: 'fr', ); expect(user.toJson(), { - 'id': 'bbb19d9a-ee50-45bc-84e5-0584e79d0c9e', + 'id': id, + 'name': name, + 'image': image, + 'extraDataStringTest': extraDataStringTest, + 'extraDataIntTest': extraDataIntTest, + 'extraDataDoubleTest': extraDataDoubleTest, + 'extraDataBoolTest': extraDataBoolTest, + 'language': 'fr', }); }); @@ -29,6 +81,14 @@ void main() { expect(newUser.id, user.id); expect(newUser.role, user.role); expect(newUser.name, user.name); + expect(newUser.image, user.image); + expect(newUser.online, user.online); + expect(newUser.banned, user.banned); + expect(newUser.teams, user.teams); + expect(newUser.createdAt, user.createdAt); + expect(newUser.updatedAt, user.updatedAt); + expect(newUser.lastActive, user.lastActive); + expect(newUser.language, user.language); newUser = user.copyWith( id: 'test', @@ -36,11 +96,94 @@ void main() { extraData: { 'name': 'test', }, + image: 'https://stream.io/new-image', + online: false, + banned: false, + teams: ['new-team1', 'new-team2'], + createdAt: DateTime.parse('2021-05-03 12:39:21.817646'), + updatedAt: DateTime.parse('2021-05-04 12:39:21.817646'), + lastActive: DateTime.parse('2021-05-06 12:39:21.817646'), + language: 'it', ); expect(newUser.id, 'test'); expect(newUser.role, 'test'); expect(newUser.name, 'test'); + expect(newUser.image, 'https://stream.io/new-image'); + expect(newUser.extraData['image'], 'https://stream.io/new-image'); + expect(newUser.online, false); + expect(newUser.banned, false); + expect(newUser.teams, ['new-team1', 'new-team2']); + expect(newUser.createdAt, DateTime.parse('2021-05-03 12:39:21.817646')); + expect(newUser.updatedAt, DateTime.parse('2021-05-04 12:39:21.817646')); + expect(newUser.lastActive, DateTime.parse('2021-05-06 12:39:21.817646')); + expect(newUser.language, 'it'); + }); + + test('image property and extraData manipulation', () { + final user = User(id: id, image: image); + + expect(user.image, image); + expect(user.extraData['image'], image); + expect(user.toJson(), {'id': id, 'image': image}); + expect(User.fromJson(user.toJson()).toJson(), {'id': id, 'image': image}); + + const imageURLOne = 'https://stream.io/image-one'; + var newUser = user.copyWith( + extraData: {'image': imageURLOne}, + ); + + expect(newUser.extraData['image'], imageURLOne); + expect(newUser.image, imageURLOne); + + const imageURLTwo = 'https://stream.io/image-two'; + newUser = user.copyWith( + image: imageURLTwo, + ); + + expect(newUser.extraData['image'], imageURLTwo); + expect(newUser.image, imageURLTwo); + + const imageURLThree = 'https://stream.io/image-three'; + newUser = user.copyWith( + image: imageURLThree, + extraData: {'image': imageURLThree}, + ); + + expect(newUser.extraData['image'], imageURLThree); + expect(newUser.image, imageURLThree); + }); + + test('default values, constructor', () { + final user = User(id: id); + + expect(user.id, id); + expect(user.role, null); + expect(user.name, id, reason: 'if a name is not supplied, default to id'); + expect(user.image, null); + expect(user.extraData, const {}); + expect(user.online, false); + expect(user.banned, false); + expect(user.teams, []); + expect(user.lastActive, null); + expect(user.createdAt, isNotNull); + expect(user.updatedAt, isNotNull); + }); + + test('default values, parse json', () { + final user = User.fromJson(const {'id': id}); + + expect(user.id, id); + expect(user.role, null); + expect(user.name, id, reason: 'if a name is not supplied, default to id'); + expect(user.image, null); + expect(user.extraData, const {}); + expect(user.online, false); + expect(user.banned, false); + expect(user.teams, []); + expect(user.lastActive, null); + expect(user.createdAt, isNotNull); + expect(user.updatedAt, isNotNull); }); }); } From 1c3927101513e9012ffb6429aeab0ecb0f2439f9 Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Wed, 4 Aug 2021 17:30:56 +0200 Subject: [PATCH 056/121] docs: fix links, grammar --- .../docs/Flutter/basics/introduction.mdx | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/docusaurus/docs/Flutter/basics/introduction.mdx b/docusaurus/docs/Flutter/basics/introduction.mdx index 756bd33f..ad1df5af 100644 --- a/docusaurus/docs/Flutter/basics/introduction.mdx +++ b/docusaurus/docs/Flutter/basics/introduction.mdx @@ -8,13 +8,14 @@ Exploring The Basics Of Stream Chat ![](../assets/sdk_title.png) -Stream Chat is a service that helps you easily build a full chat experience in your Flutter (and more) apps. +Stream Chat is a service that helps you easily build a full chat experience in your Flutter apps. +We also support a variety of other SDKs. This section of the documentation focuses on our Flutter SDK which helps you easily -ship high quality messaging experiences in apps and programs built with the [Flutter toolkit made +ship high quality messaging experiences in apps and programs built with the [Flutter toolkit by Google](https://flutter.dev). -The Stream Chat Flutter SDK comprises of four different packages to choose from ranging from ones +The Stream Chat Flutter SDK comprises five different packages to choose from, ranging from ones giving you complete control to ones that give you a rich out-of-the-box chat experience. The packages that make up the Stream Chat SDK are: @@ -30,28 +31,28 @@ reusable and customisable UI components. saving chat data locally. 5. Localizations (stream_chat_localizations): provides a set of localizations for the SDK. -We recommend building prototypes using the full UI package since it contains UI widgets already -integrated with Stream's API. [stream_chat_flutter](https://pub.dev/packages/stream_chat_flutter) -is the fastest way to get up and running using Stream chat in your app. +We recommend building prototypes using the full UI package, [stream_chat_flutter](https://pub.dev/packages/stream_chat_flutter), +since it contains UI widgets already integrated with Stream's API. Is the fastest way to get up +and running using Stream chat in your app. The Flutter SDK enables you to build any type of chat or messaging experience for Android, iOS, Web and Desktop. If you're building a very custom UI and would prefer a more lean package, -our [core package](https://pub.dev/packages/stream_chat_flutter) will be suited to this use case. Core allows you to build custom, -expressive UIs while retaining the benefits of our full Flutter SDK. -APIs for accessing and controlling users, sending messages, etc are seamlessly integrated into -this package and accessible via providers and builders. +[stream_chat_flutter_core](https://pub.dev/packages/stream_chat_flutter_core) will be suited to this +use case. Core allows you to build custom, expressive UIs while retaining the benefits of our full +Flutter SDK. APIs for accessing and controlling users, sending messages, and so forth are seamlessly integrated +into this package and accessible via providers and builders. Before going into the docs, let's take a small detour to look at how the elements of Stream Chat are structured. ### Basic Structure -There are two core elements in chat, Users and Channels. +There are two core elements in chat, Users and Channels. Channels are groups of one or more users that can message each other. In an app, you need to have a user connected to query channels. -There is no specific distinction between a chat between two people and a group chat, +There is no specific distinction between a chat with only two people and a group chat, but there is a way to create a unique chat between a certain number of people by creating a distinct channel. ![](../assets/chat_basics.png) @@ -68,7 +69,7 @@ While this is a simplistic overview of the service, the Flutter SDK handles the Before reading the docs, consider trying our [online API tour](https://getstream.io/chat/get_started/), it is a nice way to learn how the API works. -It's in-browser so Javascript-based but the ideas are pretty much the same as Dart. +It's in-browser so you'll need to use Javascript but the core conceps are pretty much the same as Dart. You may also like to look at the [Flutter tutorial](https://getstream.io/chat/flutter/tutorial/) which focuses on using the UI package to get Stream Chat integrated into a Flutter app. From 55cb5ead04703670d79344ea087528aace454308 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Wed, 4 Aug 2021 11:34:30 -0400 Subject: [PATCH 057/121] chore: use ChannelPreviewTheme.of(context) in message_search_item.dart --- .../stream_chat_flutter/lib/src/message_search_item.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_search_item.dart b/packages/stream_chat_flutter/lib/src/message_search_item.dart index 46413706..44b97c83 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_item.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_item.dart @@ -36,7 +36,7 @@ class MessageSearchItem extends StatelessWidget { final channel = getMessageResponse.channel; final channelName = channel?.extraData['name']; final user = message.user!; - final chatThemeData = StreamChatTheme.of(context); + final channelPreviewTheme = ChannelPreviewTheme.of(context); return ListTile( onTap: onTap, leading: UserAvatar( @@ -53,18 +53,18 @@ class MessageSearchItem extends StatelessWidget { user.id == StreamChat.of(context).currentUser?.id ? context.translations.youText : user.name, - style: chatThemeData.channelPreviewTheme.titleStyle, + style: channelPreviewTheme.titleStyle, ), if (channelName != null) ...[ Text( ' ${context.translations.inText} ', - style: chatThemeData.channelPreviewTheme.titleStyle?.copyWith( + style: channelPreviewTheme.titleStyle?.copyWith( fontWeight: FontWeight.normal, ), ), Text( channelName as String, - style: chatThemeData.channelPreviewTheme.titleStyle, + style: channelPreviewTheme.titleStyle, ), ], ], From 3cddec630122dfe89fea816b604f6d4a31bab8ce Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Wed, 4 Aug 2021 17:34:34 +0200 Subject: [PATCH 058/121] docs: mention localization --- CONTRIBUTING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0faad799..3bb5a923 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,6 +52,8 @@ Stream's Flutter code is kept in a single mono-repository consisting of multiple `stream_chat_persistence` - This package provides a persistence client for fetching and saving chat data locally. Stream Chat Persistence uses Moor as a disk cache. +`stream_chat_localizations` - This package provides a set of localizations for the SDK. + ### Local Setup Congratulations! 🎉. You've successfully cloned our repo, and you are ready to make your first contribution. Before you can start making code changes, there are a few things to configure. From 70c77e70e5184235b68da244e3ddb5b337d1b395 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Wed, 4 Aug 2021 11:36:58 -0400 Subject: [PATCH 059/121] chore: use ChannelHeaderTheme.of(context) in thread_header.dart --- .../stream_chat_flutter/lib/src/thread_header.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/thread_header.dart b/packages/stream_chat_flutter/lib/src/thread_header.dart index d7c6f002..66b6b8be 100644 --- a/packages/stream_chat_flutter/lib/src/thread_header.dart +++ b/packages/stream_chat_flutter/lib/src/thread_header.dart @@ -104,7 +104,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { - final chatThemeData = StreamChatTheme.of(context); + final channelHeaderTheme = ChannelHeaderTheme.of(context); final defaultSubtitle = subtitle ?? Row( @@ -113,11 +113,11 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { children: [ Text( '${context.translations.withText} ', - style: chatThemeData.channelHeaderTheme.subtitleStyle, + style: channelHeaderTheme.subtitleStyle, ), Flexible( child: ChannelName( - textStyle: chatThemeData.channelHeaderTheme.subtitleStyle, + textStyle: channelHeaderTheme.subtitleStyle, ), ), ], @@ -136,7 +136,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { showUnreads: true, ) : const SizedBox()), - backgroundColor: chatThemeData.channelHeaderTheme.color, + backgroundColor: channelHeaderTheme.color, centerTitle: true, actions: actions, title: InkWell( @@ -150,14 +150,14 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { title ?? Text( context.translations.threadReplyLabel, - style: chatThemeData.channelHeaderTheme.titleStyle, + style: channelHeaderTheme.titleStyle, ), const SizedBox(height: 2), if (showTypingIndicator) TypingIndicator( alignment: Alignment.center, channel: StreamChannel.of(context).channel, - style: chatThemeData.channelHeaderTheme.subtitleStyle, + style: channelHeaderTheme.subtitleStyle, parentId: parent.id, alternativeWidget: defaultSubtitle, ) From f0cf4fd6dbae98f89650b93953efc04418e0aa2b Mon Sep 17 00:00:00 2001 From: groovinchip Date: Wed, 4 Aug 2021 11:40:10 -0400 Subject: [PATCH 060/121] chore: use ChannelPreviewTheme.of(context) in channel_bottom_sheet.dart --- .../stream_chat_flutter/lib/src/channel_bottom_sheet.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart index 1442aac8..5723234d 100644 --- a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart +++ b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart @@ -19,6 +19,7 @@ class _ChannelBottomSheetState extends State { bool _showActions = true; late StreamChannelState _streamChannelState; + late ChannelPreviewThemeData _channelPreviewThemeData; late StreamChatThemeData _streamChatThemeData; late StreamChatState _streamChatState; @@ -64,8 +65,7 @@ class _ChannelBottomSheetState extends State { child: ChannelInfo( showTypingIndicator: false, channel: _streamChannelState.channel, - textStyle: - _streamChatThemeData.channelPreviewTheme.subtitleStyle, + textStyle: _channelPreviewThemeData.subtitleStyle, ), ), const SizedBox( @@ -213,6 +213,7 @@ class _ChannelBottomSheetState extends State { void didChangeDependencies() { _streamChannelState = StreamChannel.of(context); _streamChatThemeData = StreamChatTheme.of(context); + _channelPreviewThemeData = ChannelPreviewTheme.of(context); _streamChatState = StreamChat.of(context); super.didChangeDependencies(); } From 3ef2c4027c2189fd3f5cad3a0349e029e853b648 Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Wed, 4 Aug 2021 17:55:56 +0200 Subject: [PATCH 061/121] docs: grammar fixes --- docusaurus/docs/Flutter/basics/introduction.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docusaurus/docs/Flutter/basics/introduction.mdx b/docusaurus/docs/Flutter/basics/introduction.mdx index ad1df5af..99170774 100644 --- a/docusaurus/docs/Flutter/basics/introduction.mdx +++ b/docusaurus/docs/Flutter/basics/introduction.mdx @@ -13,7 +13,7 @@ We also support a variety of other SDKs. This section of the documentation focuses on our Flutter SDK which helps you easily ship high quality messaging experiences in apps and programs built with the [Flutter toolkit -by Google](https://flutter.dev). +made by Google](https://flutter.dev). The Stream Chat Flutter SDK comprises five different packages to choose from, ranging from ones giving you complete control to ones that give you a rich out-of-the-box chat experience. @@ -32,7 +32,7 @@ saving chat data locally. 5. Localizations (stream_chat_localizations): provides a set of localizations for the SDK. We recommend building prototypes using the full UI package, [stream_chat_flutter](https://pub.dev/packages/stream_chat_flutter), -since it contains UI widgets already integrated with Stream's API. Is the fastest way to get up +since it contains UI widgets already integrated with Stream's API. It is the fastest way to get up and running using Stream chat in your app. The Flutter SDK enables you to build any type of chat or messaging experience for Android, iOS, Web From 8a4632e6e23d24b0208dbe8998c27435d2d279c0 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Wed, 4 Aug 2021 12:03:37 -0400 Subject: [PATCH 062/121] chore: address PR feedback --- .../example/lib/tutorial_part_3.dart | 5 +---- .../lib/src/channel_header.dart | 18 ++++++++---------- .../lib/src/channel_info.dart | 2 +- .../lib/src/channel_list_header.dart | 5 +++-- .../lib/src/channel_preview.dart | 10 ++++------ .../lib/src/full_screen_media.dart | 4 +--- .../lib/src/message_actions_modal.dart | 3 +-- .../lib/src/message_list_view.dart | 2 +- .../lib/src/message_search_item.dart | 6 +++--- 9 files changed, 23 insertions(+), 32 deletions(-) diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart index a6e80b57..824c0efa 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart @@ -118,10 +118,7 @@ class ChannelListPage extends StatelessWidget { channel: channel, ), title: ChannelName( - textStyle: StreamChatTheme.of(context) - .channelPreviewTheme - .titleStyle! - .copyWith( + textStyle: ChannelPreviewTheme.of(context).titleStyle!.copyWith( color: StreamChatTheme.of(context) .colorTheme .textHighEmphasis diff --git a/packages/stream_chat_flutter/lib/src/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel_header.dart index 26418a91..11dfc95a 100644 --- a/packages/stream_chat_flutter/lib/src/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_header.dart @@ -105,7 +105,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { final channel = StreamChannel.of(context).channel; - final chatThemeData = StreamChatTheme.of(context); + final channelHeaderTheme = ChannelHeaderTheme.of(context); final leadingWidget = leading ?? (showBackButton @@ -141,17 +141,17 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { brightness: Theme.of(context).brightness, elevation: 1, leading: leadingWidget, - backgroundColor: chatThemeData.channelHeaderTheme.color, + backgroundColor: channelHeaderTheme.color, actions: actions ?? [ Padding( padding: const EdgeInsets.only(right: 10), child: Center( child: ChannelAvatar( - borderRadius: chatThemeData - .channelHeaderTheme.avatarTheme?.borderRadius, - constraints: chatThemeData - .channelHeaderTheme.avatarTheme?.constraints, + borderRadius: + channelHeaderTheme.avatarTheme?.borderRadius, + constraints: + channelHeaderTheme.avatarTheme?.constraints, onTap: onImageTap, ), ), @@ -168,16 +168,14 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { children: [ title ?? ChannelName( - textStyle: - chatThemeData.channelHeaderTheme.titleStyle, + textStyle: channelHeaderTheme.titleStyle, ), const SizedBox(height: 2), subtitle ?? ChannelInfo( showTypingIndicator: showTypingIndicator, channel: channel, - textStyle: - chatThemeData.channelHeaderTheme.subtitleStyle, + textStyle: channelHeaderTheme.subtitleStyle, ), ], ), diff --git a/packages/stream_chat_flutter/lib/src/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel_info.dart index e5c2f635..de9d74b9 100644 --- a/packages/stream_chat_flutter/lib/src/channel_info.dart +++ b/packages/stream_chat_flutter/lib/src/channel_info.dart @@ -65,7 +65,7 @@ class ChannelInfo extends StatelessWidget { } alternativeWidget = Text( text, - style: StreamChatTheme.of(context).channelHeaderTheme.subtitleStyle, + style: ChannelHeaderTheme.of(context).subtitleStyle, ); } else { final userId = StreamChat.of(context).currentUser?.id; diff --git a/packages/stream_chat_flutter/lib/src/channel_list_header.dart b/packages/stream_chat_flutter/lib/src/channel_list_header.dart index 5832f3ba..692db477 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_header.dart @@ -241,12 +241,13 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { StreamChatClient client, ) { final chatThemeData = StreamChatTheme.of(context); + final channelListHeaderTheme = ChannelListHeaderTheme.of(context); return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( context.translations.offlineLabel, - style: chatThemeData.channelListHeaderTheme.titleStyle?.copyWith( + style: channelListHeaderTheme.titleStyle?.copyWith( fontSize: 16, fontWeight: FontWeight.bold, ), @@ -257,7 +258,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { ..openConnection(), child: Text( context.translations.tryAgainLabel, - style: chatThemeData.channelListHeaderTheme.titleStyle?.copyWith( + style: channelListHeaderTheme.titleStyle?.copyWith( fontSize: 16, fontWeight: FontWeight.bold, color: chatThemeData.colorTheme.accentPrimary, diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index 3127bcbb..6aa7684e 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -183,15 +183,13 @@ class ChannelPreview extends StatelessWidget { return Text( stringDate, - style: StreamChatTheme.of(context) - .channelPreviewTheme - .lastMessageAtStyle, + style: ChannelPreviewTheme.of(context).lastMessageAtStyle, ); }, ); Widget _buildSubtitle(BuildContext context) { - final chatThemeData = StreamChatTheme.of(context); + final channelPreviewTheme = ChannelPreviewTheme.of(context); if (channel.isMuted) { return Row( crossAxisAlignment: CrossAxisAlignment.end, @@ -201,7 +199,7 @@ class ChannelPreview extends StatelessWidget { ), Text( ' ${context.translations.channelIsMutedText}', - style: chatThemeData.channelPreviewTheme.subtitleStyle, + style: channelPreviewTheme.subtitleStyle, ), ], ); @@ -209,7 +207,7 @@ class ChannelPreview extends StatelessWidget { return TypingIndicator( channel: channel, alternativeWidget: _buildLastMessage(context), - style: chatThemeData.channelPreviewTheme.subtitleStyle, + style: channelPreviewTheme.subtitleStyle, ); } diff --git a/packages/stream_chat_flutter/lib/src/full_screen_media.dart b/packages/stream_chat_flutter/lib/src/full_screen_media.dart index 63b77d16..bef2a531 100644 --- a/packages/stream_chat_flutter/lib/src/full_screen_media.dart +++ b/packages/stream_chat_flutter/lib/src/full_screen_media.dart @@ -124,9 +124,7 @@ class _FullScreenMediaState extends State ), backgroundDecoration: BoxDecoration( color: ColorTween( - begin: StreamChatTheme.of(context) - .channelHeaderTheme - .color, + begin: ChannelHeaderTheme.of(context).color, end: Colors.black, ).lerp(_controller.value), ), diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart index f1fe967b..387e31a1 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -566,8 +566,7 @@ class _MessageActionsModalState extends State { elevation: 2, clipBehavior: Clip.hardEdge, isScrollControlled: true, - backgroundColor: - streamChatThemeData.messageInputTheme.inputBackgroundColor, + backgroundColor: MessageInputTheme.of(context).inputBackgroundColor, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular(16), diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index f6f9c309..a3830966 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -629,7 +629,7 @@ class _MessageListViewState extends State { child: Text( context.translations.threadSeparatorText(replyCount), textAlign: TextAlign.center, - style: _streamTheme.channelHeaderTheme.subtitleStyle, + style: ChannelHeaderTheme.of(context).subtitleStyle, ), ), ); diff --git a/packages/stream_chat_flutter/lib/src/message_search_item.dart b/packages/stream_chat_flutter/lib/src/message_search_item.dart index 44b97c83..ab41532c 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_item.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_item.dart @@ -122,18 +122,18 @@ class MessageSearchItem extends StatelessWidget { text = parts.join(' '); } - final chatThemeData = StreamChatTheme.of(context); + final channelPreviewTheme = ChannelPreviewTheme.of(context); return Text.rich( _getDisplayText( text!, message.mentionedUsers, message.attachments, - chatThemeData.channelPreviewTheme.subtitleStyle?.copyWith( + channelPreviewTheme.subtitleStyle?.copyWith( fontStyle: (message.isSystem || message.isDeleted) ? FontStyle.italic : FontStyle.normal, ), - chatThemeData.channelPreviewTheme.subtitleStyle?.copyWith( + channelPreviewTheme.subtitleStyle?.copyWith( fontStyle: (message.isSystem || message.isDeleted) ? FontStyle.italic : FontStyle.normal, From 8de0eab7cb6060067fcb7821401ece1c17b3748d Mon Sep 17 00:00:00 2001 From: groovinchip Date: Wed, 4 Aug 2021 12:24:56 -0400 Subject: [PATCH 063/121] chore: update CHANGELOG.md --- packages/stream_chat_flutter/CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 81e6235e..efac9508 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,21 @@ +## Upcoming + +🔄 Changed + +Theming has been upgraded! Most theme classes now have `InheritedTheme` classes +associated with them, and have been upgraded with some goodies like `lerp` functions. +Here's the full naming breakdown: +* `AvatarTheme` is now `AvatarThemeData` +* `ChannelHeaderTheme` is now `ChannelHeaderThemeData` +* `ChannelListHeaderTheme` is now `ChannelListHeaderThemeData` +* `ChannelListViewTheme` is now `ChannelListViewThemeData` +* `ChannelPreviewTheme` is now `ChannelPreviewThemeData` +* `MessageInputTheme` is now `MessageInputThemeData` +* `MessageListViewTheme` is now `MessageListViewTheme` +* `MessageSearchListViewTheme` is now `MessageSearchListViewThemeData` +* `MessageTheme` is now `MessageThemeData` +* `UserListViewTheme` is now `UserListViewThemeData` + ## 2.1.2 🐞 Fixed @@ -531,6 +549,7 @@ Widget build(BuildContext context) { }, home: ChannelListPage(), ); +} ``` - Fix reaction bubble going below previous message on iOS From d74576a1971465865e75e9ffbb541c0c8ec1f7c5 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Wed, 4 Aug 2021 14:35:58 -0400 Subject: [PATCH 064/121] chore: add UserAvatar placeholder builder to stream_chat_theme.dart and user_avatar.dart --- packages/stream_chat_flutter/lib/src/stream_chat_theme.dart | 4 ++++ packages/stream_chat_flutter/lib/src/user_avatar.dart | 2 ++ 2 files changed, 6 insertions(+) diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index 5fe56dbc..902f5271 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -113,6 +113,7 @@ class StreamChatThemeData { required this.ownMessageTheme, required this.messageInputTheme, required this.defaultUserImage, + this.placeholderUserImage, required this.primaryIconTheme, required this.reactionIcons, required this.galleryHeaderTheme, @@ -170,6 +171,9 @@ class StreamChatThemeData { /// The widget that will be built when the user image is unavailable final Widget Function(BuildContext, User) defaultUserImage; + /// The widget that will be built when the user image is loading + final Widget Function(BuildContext context, User user)? placeholderUserImage; + /// Primary icon theme final IconThemeData primaryIconTheme; diff --git a/packages/stream_chat_flutter/lib/src/user_avatar.dart b/packages/stream_chat_flutter/lib/src/user_avatar.dart index 31e07f0a..ba737e5e 100644 --- a/packages/stream_chat_flutter/lib/src/user_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/user_avatar.dart @@ -79,6 +79,8 @@ class UserAvatar extends StatelessWidget { imageUrl: user.extraData['image'] as String, errorWidget: (_, __, ___) => streamChatTheme.defaultUserImage(context, user), + placeholder: (_, __) => + streamChatTheme.placeholderUserImage!(context, user), fit: BoxFit.cover, ) : streamChatTheme.defaultUserImage(context, user), From 1b9c718ed880e785ad0e36bb330864e8d71b23b1 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Wed, 4 Aug 2021 15:34:48 -0400 Subject: [PATCH 065/121] chore: address PR feedback --- .../stream_chat_flutter/lib/src/user_avatar.dart | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/user_avatar.dart b/packages/stream_chat_flutter/lib/src/user_avatar.dart index ba737e5e..7c9437eb 100644 --- a/packages/stream_chat_flutter/lib/src/user_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/user_avatar.dart @@ -19,6 +19,7 @@ class UserAvatar extends StatelessWidget { this.selected = false, this.selectionColor, this.selectionThickness = 4, + this.placeholderUserImageBuilder, }) : super(key: key); /// User whose avatar is to displayed @@ -54,6 +55,10 @@ class UserAvatar extends StatelessWidget { /// Selection thickness around the avatar final double selectionThickness; + /// The widget that will be built when the user image is loading + final Widget Function(BuildContext context, User user)? + placeholderUserImageBuilder; + @override Widget build(BuildContext context) { final hasImage = user.extraData.containsKey('image') && @@ -79,8 +84,12 @@ class UserAvatar extends StatelessWidget { imageUrl: user.extraData['image'] as String, errorWidget: (_, __, ___) => streamChatTheme.defaultUserImage(context, user), - placeholder: (_, __) => - streamChatTheme.placeholderUserImage!(context, user), + placeholder: placeholderUserImageBuilder == null + ? streamChatTheme.placeholderUserImage == null + ? null + : (_, __) => streamChatTheme.placeholderUserImage!( + context, user) + : (_, __) => placeholderUserImageBuilder!(context, user), fit: BoxFit.cover, ) : streamChatTheme.defaultUserImage(context, user), From 1f48e571603af9e388c205b1739d70862b51d60e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 5 Aug 2021 13:06:11 +0530 Subject: [PATCH 066/121] chore(ui): address PR feedback Signed-off-by: xsahil03x --- .../stream_chat_flutter/lib/src/channel_preview.dart | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index 6aa7684e..333ee886 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -242,20 +242,19 @@ class ChannelPreview extends StatelessWidget { text = parts.join(' '); - final chatThemeData = StreamChatTheme.of(context); + final channelPreviewTheme = ChannelPreviewTheme.of(context); return Text.rich( _getDisplayText( text, lastMessage.mentionedUsers, lastMessage.attachments, - chatThemeData.channelPreviewTheme.subtitleStyle?.copyWith( - color: - chatThemeData.channelPreviewTheme.subtitleStyle?.color, + channelPreviewTheme.subtitleStyle?.copyWith( + color: channelPreviewTheme.subtitleStyle?.color, fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) ? FontStyle.italic : FontStyle.normal), - chatThemeData.channelPreviewTheme.subtitleStyle?.copyWith( - color: chatThemeData.channelPreviewTheme.subtitleStyle?.color, + channelPreviewTheme.subtitleStyle?.copyWith( + color: channelPreviewTheme.subtitleStyle?.color, fontStyle: (lastMessage.isSystem || lastMessage.isDeleted) ? FontStyle.italic : FontStyle.normal, From f3b7f31badba82e26f3e1c8369e821422aad3976 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 5 Aug 2021 14:54:30 +0530 Subject: [PATCH 067/121] chore(ui): minor improvements Signed-off-by: xsahil03x --- .../lib/src/stream_chat_theme.dart | 266 +++++++++--------- .../lib/src/user_avatar.dart | 21 +- 2 files changed, 145 insertions(+), 142 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index 902f5271..f7a05d7e 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -54,6 +54,7 @@ class StreamChatThemeData { MessageThemeData? ownMessageTheme, MessageInputThemeData? messageInputTheme, Widget Function(BuildContext, User)? defaultUserImage, + Widget Function(BuildContext, User)? placeholderUserImage, IconThemeData? primaryIconTheme, List? reactionIcons, GalleryHeaderThemeData? imageHeaderTheme, @@ -68,7 +69,7 @@ class StreamChatThemeData { textTheme ??= isDark ? TextTheme.dark() : TextTheme.light(); colorTheme ??= isDark ? ColorTheme.dark() : ColorTheme.light(); - final defaultData = fromColorAndTextTheme( + final defaultData = StreamChatThemeData.fromColorAndTextTheme( colorTheme, textTheme, ); @@ -81,6 +82,7 @@ class StreamChatThemeData { ownMessageTheme: ownMessageTheme, messageInputTheme: messageInputTheme, defaultUserImage: defaultUserImage, + placeholderUserImage: placeholderUserImage, primaryIconTheme: primaryIconTheme, reactionIcons: reactionIcons, galleryHeaderTheme: imageHeaderTheme, @@ -136,137 +138,8 @@ class StreamChatThemeData { return defaultTheme.merge(customizedTheme); } - /// The text themes used in the widgets - final TextTheme textTheme; - - /// The color themes used in the widgets - final ColorTheme colorTheme; - - /// Theme of the [ChannelPreview] - final ChannelPreviewThemeData channelPreviewTheme; - - /// Theme of the [ChannelListHeader] - final ChannelListHeaderThemeData channelListHeaderTheme; - - /// Theme of the chat widgets dedicated to a channel header - final ChannelHeaderThemeData channelHeaderTheme; - - /// 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 - final MessageThemeData ownMessageTheme; - - /// Theme of other users messages - final MessageThemeData otherMessageTheme; - - /// Theme dedicated to the [MessageInput] widget - final MessageInputThemeData messageInputTheme; - - /// The widget that will be built when the user image is unavailable - final Widget Function(BuildContext, User) defaultUserImage; - - /// The widget that will be built when the user image is loading - final Widget Function(BuildContext context, User user)? placeholderUserImage; - - /// Primary icon theme - final IconThemeData primaryIconTheme; - - /// Assets used for rendering reactions - final List reactionIcons; - - /// Theme configuration for the [MessageListView] widget. - final MessageListViewThemeData messageListViewTheme; - - /// Theme configuration for the [ChannelListView] widget. - final ChannelListViewThemeData channelListViewTheme; - - /// Theme configuration for the [UserListView] widget. - final UserListViewThemeData userListViewTheme; - - /// Theme configuration for the [MessageSearchListView] widget. - final MessageSearchListViewThemeData messageSearchListViewTheme; - - /// Creates a copy of [StreamChatThemeData] with specified attributes - /// overridden. - StreamChatThemeData copyWith({ - TextTheme? textTheme, - ColorTheme? colorTheme, - ChannelPreviewThemeData? channelPreviewTheme, - ChannelHeaderThemeData? channelHeaderTheme, - MessageThemeData? ownMessageTheme, - MessageThemeData? otherMessageTheme, - MessageInputThemeData? messageInputTheme, - Widget Function(BuildContext, User)? defaultUserImage, - IconThemeData? primaryIconTheme, - ChannelListHeaderThemeData? channelListHeaderTheme, - List? reactionIcons, - GalleryHeaderThemeData? galleryHeaderTheme, - GalleryFooterThemeData? galleryFooterTheme, - MessageListViewThemeData? messageListViewTheme, - ChannelListViewThemeData? channelListViewTheme, - UserListViewThemeData? userListViewTheme, - MessageSearchListViewThemeData? messageSearchListViewTheme, - }) => - StreamChatThemeData.raw( - channelListHeaderTheme: - this.channelListHeaderTheme.merge(channelListHeaderTheme), - textTheme: this.textTheme.merge(textTheme), - colorTheme: this.colorTheme.merge(colorTheme), - primaryIconTheme: this.primaryIconTheme.merge(primaryIconTheme), - defaultUserImage: defaultUserImage ?? this.defaultUserImage, - channelPreviewTheme: - this.channelPreviewTheme.merge(channelPreviewTheme), - channelHeaderTheme: this.channelHeaderTheme.merge(channelHeaderTheme), - ownMessageTheme: this.ownMessageTheme.merge(ownMessageTheme), - otherMessageTheme: this.otherMessageTheme.merge(otherMessageTheme), - messageInputTheme: this.messageInputTheme.merge(messageInputTheme), - reactionIcons: reactionIcons ?? this.reactionIcons, - galleryHeaderTheme: galleryHeaderTheme ?? this.galleryHeaderTheme, - galleryFooterTheme: galleryFooterTheme ?? this.galleryFooterTheme, - messageListViewTheme: messageListViewTheme ?? this.messageListViewTheme, - channelListViewTheme: channelListViewTheme ?? this.channelListViewTheme, - userListViewTheme: userListViewTheme ?? this.userListViewTheme, - messageSearchListViewTheme: - messageSearchListViewTheme ?? this.messageSearchListViewTheme, - ); - - /// Merge themes - StreamChatThemeData merge(StreamChatThemeData? other) { - if (other == null) return this; - return copyWith( - channelListHeaderTheme: - channelListHeaderTheme.merge(other.channelListHeaderTheme), - textTheme: textTheme.merge(other.textTheme), - colorTheme: colorTheme.merge(other.colorTheme), - primaryIconTheme: other.primaryIconTheme, - defaultUserImage: other.defaultUserImage, - channelPreviewTheme: channelPreviewTheme.merge(other.channelPreviewTheme), - channelHeaderTheme: channelHeaderTheme.merge(other.channelHeaderTheme), - ownMessageTheme: ownMessageTheme.merge(other.ownMessageTheme), - otherMessageTheme: otherMessageTheme.merge(other.otherMessageTheme), - messageInputTheme: messageInputTheme.merge(other.messageInputTheme), - reactionIcons: other.reactionIcons, - galleryHeaderTheme: galleryHeaderTheme.merge(other.galleryHeaderTheme), - galleryFooterTheme: galleryFooterTheme.merge(other.galleryFooterTheme), - messageListViewTheme: - messageListViewTheme.merge(other.messageListViewTheme), - channelListViewTheme: - channelListViewTheme.merge(other.channelListViewTheme), - userListViewTheme: userListViewTheme.merge(other.userListViewTheme), - messageSearchListViewTheme: - messageSearchListViewTheme.merge(other.messageSearchListViewTheme), - ); - } - /// Create theme from color and text theme - // ignore: prefer_constructors_over_static_methods - static StreamChatThemeData fromColorAndTextTheme( + factory StreamChatThemeData.fromColorAndTextTheme( ColorTheme colorTheme, TextTheme textTheme, ) { @@ -491,4 +364,135 @@ class StreamChatThemeData { ), ); } + + /// The text themes used in the widgets + final TextTheme textTheme; + + /// The color themes used in the widgets + final ColorTheme colorTheme; + + /// Theme of the [ChannelPreview] + final ChannelPreviewThemeData channelPreviewTheme; + + /// Theme of the [ChannelListHeader] + final ChannelListHeaderThemeData channelListHeaderTheme; + + /// Theme of the chat widgets dedicated to a channel header + final ChannelHeaderThemeData channelHeaderTheme; + + /// 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 + final MessageThemeData ownMessageTheme; + + /// Theme of other users messages + final MessageThemeData otherMessageTheme; + + /// Theme dedicated to the [MessageInput] widget + final MessageInputThemeData messageInputTheme; + + /// The widget that will be built when the user image is unavailable + final Widget Function(BuildContext, User) defaultUserImage; + + /// The widget that will be built when the user image is loading + final Widget Function(BuildContext, User)? placeholderUserImage; + + /// Primary icon theme + final IconThemeData primaryIconTheme; + + /// Assets used for rendering reactions + final List reactionIcons; + + /// Theme configuration for the [MessageListView] widget. + final MessageListViewThemeData messageListViewTheme; + + /// Theme configuration for the [ChannelListView] widget. + final ChannelListViewThemeData channelListViewTheme; + + /// Theme configuration for the [UserListView] widget. + final UserListViewThemeData userListViewTheme; + + /// Theme configuration for the [MessageSearchListView] widget. + final MessageSearchListViewThemeData messageSearchListViewTheme; + + /// Creates a copy of [StreamChatThemeData] with specified attributes + /// overridden. + StreamChatThemeData copyWith({ + TextTheme? textTheme, + ColorTheme? colorTheme, + ChannelPreviewThemeData? channelPreviewTheme, + ChannelHeaderThemeData? channelHeaderTheme, + MessageThemeData? ownMessageTheme, + MessageThemeData? otherMessageTheme, + MessageInputThemeData? messageInputTheme, + Widget Function(BuildContext, User)? defaultUserImage, + Widget Function(BuildContext, User)? placeholderUserImage, + IconThemeData? primaryIconTheme, + ChannelListHeaderThemeData? channelListHeaderTheme, + List? reactionIcons, + GalleryHeaderThemeData? galleryHeaderTheme, + GalleryFooterThemeData? galleryFooterTheme, + MessageListViewThemeData? messageListViewTheme, + ChannelListViewThemeData? channelListViewTheme, + UserListViewThemeData? userListViewTheme, + MessageSearchListViewThemeData? messageSearchListViewTheme, + }) => + StreamChatThemeData.raw( + channelListHeaderTheme: + this.channelListHeaderTheme.merge(channelListHeaderTheme), + textTheme: this.textTheme.merge(textTheme), + colorTheme: this.colorTheme.merge(colorTheme), + primaryIconTheme: this.primaryIconTheme.merge(primaryIconTheme), + defaultUserImage: defaultUserImage ?? this.defaultUserImage, + placeholderUserImage: placeholderUserImage ?? this.placeholderUserImage, + channelPreviewTheme: + this.channelPreviewTheme.merge(channelPreviewTheme), + channelHeaderTheme: this.channelHeaderTheme.merge(channelHeaderTheme), + ownMessageTheme: this.ownMessageTheme.merge(ownMessageTheme), + otherMessageTheme: this.otherMessageTheme.merge(otherMessageTheme), + messageInputTheme: this.messageInputTheme.merge(messageInputTheme), + reactionIcons: reactionIcons ?? this.reactionIcons, + galleryHeaderTheme: galleryHeaderTheme ?? this.galleryHeaderTheme, + galleryFooterTheme: galleryFooterTheme ?? this.galleryFooterTheme, + messageListViewTheme: messageListViewTheme ?? this.messageListViewTheme, + channelListViewTheme: channelListViewTheme ?? this.channelListViewTheme, + userListViewTheme: userListViewTheme ?? this.userListViewTheme, + messageSearchListViewTheme: + messageSearchListViewTheme ?? this.messageSearchListViewTheme, + ); + + /// Merge themes + StreamChatThemeData merge(StreamChatThemeData? other) { + if (other == null) return this; + return copyWith( + channelListHeaderTheme: + channelListHeaderTheme.merge(other.channelListHeaderTheme), + textTheme: textTheme.merge(other.textTheme), + colorTheme: colorTheme.merge(other.colorTheme), + primaryIconTheme: other.primaryIconTheme, + defaultUserImage: other.defaultUserImage, + placeholderUserImage: other.placeholderUserImage, + channelPreviewTheme: channelPreviewTheme.merge(other.channelPreviewTheme), + channelHeaderTheme: channelHeaderTheme.merge(other.channelHeaderTheme), + ownMessageTheme: ownMessageTheme.merge(other.ownMessageTheme), + otherMessageTheme: otherMessageTheme.merge(other.otherMessageTheme), + messageInputTheme: messageInputTheme.merge(other.messageInputTheme), + reactionIcons: other.reactionIcons, + galleryHeaderTheme: galleryHeaderTheme.merge(other.galleryHeaderTheme), + galleryFooterTheme: galleryFooterTheme.merge(other.galleryFooterTheme), + messageListViewTheme: + messageListViewTheme.merge(other.messageListViewTheme), + channelListViewTheme: + channelListViewTheme.merge(other.channelListViewTheme), + userListViewTheme: userListViewTheme.merge(other.userListViewTheme), + messageSearchListViewTheme: + messageSearchListViewTheme.merge(other.messageSearchListViewTheme), + ); + } } diff --git a/packages/stream_chat_flutter/lib/src/user_avatar.dart b/packages/stream_chat_flutter/lib/src/user_avatar.dart index 7c9437eb..40561d77 100644 --- a/packages/stream_chat_flutter/lib/src/user_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/user_avatar.dart @@ -19,7 +19,7 @@ class UserAvatar extends StatelessWidget { this.selected = false, this.selectionColor, this.selectionThickness = 4, - this.placeholderUserImageBuilder, + this.placeholder, }) : super(key: key); /// User whose avatar is to displayed @@ -56,8 +56,7 @@ class UserAvatar extends StatelessWidget { final double selectionThickness; /// The widget that will be built when the user image is loading - final Widget Function(BuildContext context, User user)? - placeholderUserImageBuilder; + final Widget Function(BuildContext, User)? placeholder; @override Widget build(BuildContext context) { @@ -66,6 +65,9 @@ class UserAvatar extends StatelessWidget { user.extraData['image'] != ''; final streamChatTheme = StreamChatTheme.of(context); + final placeholder = + this.placeholder ?? streamChatTheme.placeholderUserImage; + Widget avatar = FittedBox( fit: BoxFit.cover, child: ClipRRect( @@ -79,18 +81,15 @@ class UserAvatar extends StatelessWidget { ), child: hasImage ? CachedNetworkImage( + fit: BoxFit.cover, filterQuality: FilterQuality.high, // ignore: cast_nullable_to_non_nullable imageUrl: user.extraData['image'] as String, - errorWidget: (_, __, ___) => + errorWidget: (context, __, ___) => streamChatTheme.defaultUserImage(context, user), - placeholder: placeholderUserImageBuilder == null - ? streamChatTheme.placeholderUserImage == null - ? null - : (_, __) => streamChatTheme.placeholderUserImage!( - context, user) - : (_, __) => placeholderUserImageBuilder!(context, user), - fit: BoxFit.cover, + placeholder: placeholder != null + ? (context, __) => placeholder(context, user) + : null, ) : streamChatTheme.defaultUserImage(context, user), ), From 29d0f91be1677752e0d53ccedca9c327d7330dc9 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 5 Aug 2021 15:02:39 +0530 Subject: [PATCH 068/121] fix(ui): remove background color from `UserAvatar` Signed-off-by: xsahil03x --- packages/stream_chat_flutter/lib/src/user_avatar.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/user_avatar.dart b/packages/stream_chat_flutter/lib/src/user_avatar.dart index 40561d77..61627a81 100644 --- a/packages/stream_chat_flutter/lib/src/user_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/user_avatar.dart @@ -76,9 +76,6 @@ class UserAvatar extends StatelessWidget { child: Container( constraints: constraints ?? streamChatTheme.ownMessageTheme.avatarTheme?.constraints, - decoration: BoxDecoration( - color: streamChatTheme.colorTheme.accentPrimary, - ), child: hasImage ? CachedNetworkImage( fit: BoxFit.cover, From 0df21e92c34f9a34812eed2ec4bcbc2c8ad118bb Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 5 Aug 2021 15:03:04 +0530 Subject: [PATCH 069/121] chore(ui): update CHANGELOG.md Signed-off-by: xsahil03x --- packages/stream_chat_flutter/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index efac9508..4fe9e6e9 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,5 +1,11 @@ ## Upcoming +✅ Added + +- [#516](https://github.com/GetStream/stream-chat-flutter/issues/516): Added `StreamChatThemeData.placeholderUserImage` for + building a widget when the `UserAvatar` image is loading + + 🔄 Changed Theming has been upgraded! Most theme classes now have `InheritedTheme` classes From 2b57076a32ddd7ac4a2ea9d65f0c43d045f25e98 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 5 Aug 2021 15:11:12 +0530 Subject: [PATCH 070/121] added new guide --- .../guides/adding_local_data_persistence.mdx | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 docusaurus/docs/Flutter/guides/adding_local_data_persistence.mdx diff --git a/docusaurus/docs/Flutter/guides/adding_local_data_persistence.mdx b/docusaurus/docs/Flutter/guides/adding_local_data_persistence.mdx new file mode 100644 index 00000000..76e84524 --- /dev/null +++ b/docusaurus/docs/Flutter/guides/adding_local_data_persistence.mdx @@ -0,0 +1,76 @@ +--- +id: adding_local_data_persistence +sidebar_position: 9 +title: Adding Local Data Persistence +--- + +Adding Local Data Persistence + +### Introduction + +Most messaging apps need to work regardless of whether the app is currently connected to the internet. +Local data persistence stores the fetched data from the backend on a local SQLite database using the +moor package in Flutter. All packages in the SDK can use local data persistence to store messages +across multiple platforms. + +### Implementation + +To add data persistence you can extend the class ChatPersistenceClient and pass an instance to the StreamChatClient. + +```dart +class CustomChatPersistentClient extends ChatPersistenceClient { +... +} + +final client = StreamChatClient( + apiKey ?? kDefaultStreamApiKey, + logLevel: Level.INFO, +)..chatPersistenceClient = CustomChatPersistentClient(); +``` + +We provide an official persistent client in the [stream_chat_persistence](https://pub.dev/packages/stream_chat_persistence) +package that works using the library [moor](https://moor.simonbinder.eu), an SQLite ORM. + +Add this to your package's `pubspec.yaml` file, using the latest version. + +```yaml +dependencies: + stream_chat_persistence: ^latest_version +``` + +You should then run `flutter packages get` + +The usage is pretty simple. + +1. Create a new instance of `StreamChatPersistenceClient` providing `logLevel` and `connectionMode` + +```dart +final chatPersistentClient = StreamChatPersistenceClient( + logLevel: Level.INFO, + connectionMode: ConnectionMode.background, +); +``` + +2. Pass the instance to the official `StreamChatClient` + +```dart + final client = StreamChatClient( + apiKey ?? kDefaultStreamApiKey, + logLevel: Level.INFO, + )..chatPersistenceClient = chatPersistentClient; +``` + +And you are ready to go... + +Note that passing `ConnectionMode.background` the database uses a background isolate to unblock the main thread. +The `StreamChatClient` uses the `chatPersistentClient` to synchronize the database with the newest +information every time it receives new data about channels/messages/users. + +### Multi-user + +The DB file is named after the `userId`, so if you instantiate a client using a different `userId` you will use a different database. +Calling `client.disconnect(flushOfflineStorage: true)` flushes all current database data. + +### Updating/deleting/sending a message while offline + +The information about the action is saved in offline storage. When the client returns online, everything is retried. \ No newline at end of file From 291d33434da1981c6ec021a70afb5ee217898a43 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 5 Aug 2021 15:15:03 +0530 Subject: [PATCH 071/121] Update docusaurus/docs/Flutter/guides/adding_local_data_persistence.mdx Co-authored-by: Sahil Kumar --- .../docs/Flutter/guides/adding_local_data_persistence.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docusaurus/docs/Flutter/guides/adding_local_data_persistence.mdx b/docusaurus/docs/Flutter/guides/adding_local_data_persistence.mdx index 76e84524..e6283ca0 100644 --- a/docusaurus/docs/Flutter/guides/adding_local_data_persistence.mdx +++ b/docusaurus/docs/Flutter/guides/adding_local_data_persistence.mdx @@ -69,8 +69,8 @@ information every time it receives new data about channels/messages/users. ### Multi-user The DB file is named after the `userId`, so if you instantiate a client using a different `userId` you will use a different database. -Calling `client.disconnect(flushOfflineStorage: true)` flushes all current database data. +Calling `client.disconnectUser(flushChatPersistence: true)` flushes all current database data. ### Updating/deleting/sending a message while offline -The information about the action is saved in offline storage. When the client returns online, everything is retried. \ No newline at end of file +The information about the action is saved in offline storage. When the client returns online, everything is retried. From 46828764c1311d3b116e8b344c6934612abac68c Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 5 Aug 2021 16:37:23 +0530 Subject: [PATCH 072/121] feat(ui): minor fixes, add support for `name` in user.dart Signed-off-by: xsahil03x --- .../lib/src/core/models/own_user.dart | 56 +++++---- .../lib/src/core/models/own_user.g.dart | 1 - .../stream_chat/lib/src/core/models/user.dart | 106 +++++++----------- .../lib/src/core/models/user.g.dart | 2 - .../test/src/core/models/user_test.dart | 40 ++++++- 5 files changed, 111 insertions(+), 94 deletions(-) diff --git a/packages/stream_chat/lib/src/core/models/own_user.dart b/packages/stream_chat/lib/src/core/models/own_user.dart index 5adc753c..ee81e909 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.dart @@ -20,6 +20,8 @@ class OwnUser extends User { this.channelMutes = const [], required String id, String? role, + String? name, + String? image, DateTime? createdAt, DateTime? updatedAt, DateTime? lastActive, @@ -28,10 +30,11 @@ class OwnUser extends User { bool banned = false, List teams = const [], String? language, - String? image, }) : super( id: id, role: role, + name: name, + image: image, createdAt: createdAt, updatedAt: updatedAt, lastActive: lastActive, @@ -40,7 +43,6 @@ class OwnUser extends User { banned: banned, teams: teams, language: language, - image: image, ); /// Create a new instance from a json @@ -51,6 +53,8 @@ class OwnUser extends User { factory OwnUser.fromUser(User user) => OwnUser( id: user.id, role: user.role, + name: user.name, + image: user.image, createdAt: user.createdAt, updatedAt: user.updatedAt, lastActive: user.lastActive, @@ -59,7 +63,6 @@ class OwnUser extends User { extraData: user.extraData, teams: user.teams, language: user.language, - image: user.image, ); /// Creates a copy of [OwnUser] with specified attributes overridden. @@ -67,6 +70,8 @@ class OwnUser extends User { OwnUser copyWith({ String? id, String? role, + String? name, + String? image, DateTime? createdAt, DateTime? updatedAt, DateTime? lastActive, @@ -80,48 +85,51 @@ class OwnUser extends User { int? totalUnreadCount, int? unreadChannels, String? language, - String? image, }) => OwnUser( - id: id ?? this.id, - banned: banned ?? this.banned, - role: role ?? this.role, - createdAt: createdAt ?? this.createdAt, - updatedAt: updatedAt ?? this.updatedAt, - lastActive: lastActive ?? this.lastActive, - online: online ?? this.online, - extraData: extraData ?? this.extraData, - teams: teams ?? this.teams, - channelMutes: channelMutes ?? this.channelMutes, - devices: devices ?? this.devices, - mutes: mutes ?? this.mutes, - totalUnreadCount: totalUnreadCount ?? this.totalUnreadCount, - unreadChannels: unreadChannels ?? this.unreadChannels, - language: language ?? this.language, - image: image // if null, it will be retrieved from extraData['image'] - ); + id: id ?? this.id, + role: role ?? this.role, + /* if null, it will be retrieved from extraData['name']*/ + name: name, + /* if null, it will be retrieved from extraData['image']*/ + image: image, + banned: banned ?? this.banned, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + lastActive: lastActive ?? this.lastActive, + online: online ?? this.online, + extraData: extraData ?? this.extraData, + teams: teams ?? this.teams, + channelMutes: channelMutes ?? this.channelMutes, + devices: devices ?? this.devices, + mutes: mutes ?? this.mutes, + totalUnreadCount: totalUnreadCount ?? this.totalUnreadCount, + unreadChannels: unreadChannels ?? this.unreadChannels, + language: language ?? this.language, + ); /// Returns a new [OwnUser] that is a combination of this ownUser /// and the given [other] ownUser. OwnUser merge(OwnUser? other) { if (other == null) return this; return copyWith( + id: other.id, + role: other.role, + name: other.name, + image: other.image, banned: other.banned, channelMutes: other.channelMutes, createdAt: other.createdAt, devices: other.devices, extraData: other.extraData, - id: other.id, lastActive: other.lastActive, mutes: other.mutes, online: other.online, - role: other.role, teams: other.teams, totalUnreadCount: other.totalUnreadCount, unreadChannels: other.unreadChannels, updatedAt: other.updatedAt, language: other.language, - image: other.image, ); } diff --git a/packages/stream_chat/lib/src/core/models/own_user.g.dart b/packages/stream_chat/lib/src/core/models/own_user.g.dart index cacb99c5..ca4acdea 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.g.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.g.dart @@ -40,6 +40,5 @@ OwnUser _$OwnUserFromJson(Map json) { (json['teams'] as List?)?.map((e) => e as String).toList() ?? [], language: json['language'] as String?, - image: json['image'] as String?, ); } diff --git a/packages/stream_chat/lib/src/core/models/user.dart b/packages/stream_chat/lib/src/core/models/user.dart index 9baaf049..5a5ef8e5 100644 --- a/packages/stream_chat/lib/src/core/models/user.dart +++ b/packages/stream_chat/lib/src/core/models/user.dart @@ -9,6 +9,18 @@ part 'user.g.dart'; class User extends Equatable { /// Creates a new user. /// + /// {@template name} + /// If an [name] is provided it will be set on [extraData] with a `key` + /// of 'name'. + /// + /// For example: + /// ```dart + /// final user = User(id: 'id', name: 'Sahil Kumar'); + /// print(user.name == user.extraData['name']); // true + /// ``` + /// {@endtemplate} + /// + /// {@template image} /// If an [image] is provided it will be set on [extraData] with a `key` /// of 'image'. /// @@ -17,9 +29,11 @@ class User extends Equatable { /// final user = User(id: 'id', image: 'https://getstream.io/image.png'); /// print(user.image == user.extraData['image']); // true /// ``` + /// {@endtemplate} User({ required this.id, this.role, + String? name, String? image, DateTime? createdAt, DateTime? updatedAt, @@ -29,13 +43,14 @@ class User extends Equatable { this.banned = false, this.teams = const [], this.language, - }) : _image = image, - createdAt = createdAt ?? DateTime.now(), + }) : createdAt = createdAt ?? DateTime.now(), updatedAt = updatedAt ?? DateTime.now(), - - // For backwards compatibalitity, set 'image' on [extraData]. - extraData = - (image != null) ? {...extraData, 'image': image} : extraData; + /*For backwards compatibility, set 'name', 'image' in [extraData].*/ + extraData = { + ...extraData, + if (name != null) 'name': name, + if (image != null) 'image': image, + }; /// Create a new instance from json. factory User.fromJson(Map json) => @@ -54,62 +69,32 @@ class User extends Equatable { 'banned', 'teams', 'language', - 'image', ]; /// User id. final String id; - /// User role. - @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) - final String? role; - - /// Image for user. This is also set on `extraData['image']`. + /// Shortcut for user name. /// - /// {@template image} - /// There are a few ways to set an image. - /// - /// Setting an image by passing in an image argument: - /// ```dart - /// final user = User( - /// id: 'id', - /// image: 'https://getstream.io/image', - /// ); - /// ``` - /// - /// Or by directly setting it in [extraData], for example: - /// ```dart - /// final user = User( - /// id: 'id', - /// extraData: const {'image': 'https://getstream.io/image'}, - /// ); - /// - /// ``` - /// Parsing json with an 'image' key will automatically set the `image` - /// property and `extraData['image']` key/value. - /// - /// ```dart - /// final user = User.fromJson({ - /// id: 'id', - /// image: 'https://getstream.io/image', // key: image - /// }); - /// - /// print(user.image == user.extraData['image']); // true - /// ``` - /// {@endtemplate} - final String? _image; + /// {@macro name} + @JsonKey(ignore: true) + String get name { + if (extraData.containsKey('name')) { + final name = extraData['name']! as String; + if (name.isNotEmpty) return name; + } + return id; + } /// Shortcut for user image. /// /// {@macro image} + @JsonKey(ignore: true) + String? get image => extraData['image'] as String?; + + /// User role. @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) - String? get image { - if (_image != null) { - return _image; - } else { - return extraData['image'] as String?; - } - } + final String? role; /// User teams @JsonKey( @@ -152,15 +137,6 @@ class User extends Equatable { @JsonKey(includeIfNull: false) final String? language; - /// Shortcut for user name. - String get name { - if (extraData.containsKey('name')) { - final name = extraData['name']! as String; - if (name.isNotEmpty) return name; - } - return id; - } - /// List of users to list of userIds. static List? toIds(List? users) => users?.map((u) => u.id).toList(); @@ -174,6 +150,8 @@ class User extends Equatable { User copyWith({ String? id, String? role, + String? name, + String? image, DateTime? createdAt, DateTime? updatedAt, DateTime? lastActive, @@ -182,11 +160,14 @@ class User extends Equatable { bool? banned, List? teams, String? language, - String? image, }) => User( id: id ?? this.id, role: role ?? this.role, + /* if null, it will be retrieved from extraData['name']*/ + name: name, + /* if null, it will be retrieved from extraData['image']*/ + image: image, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, lastActive: lastActive ?? this.lastActive, @@ -195,9 +176,8 @@ class User extends Equatable { banned: banned ?? this.banned, teams: teams ?? this.teams, language: language ?? this.language, - image: image, // if null, it will be retrieved from extraData['image'] ); @override - List get props => [id]; + List get props => [id, role]; } diff --git a/packages/stream_chat/lib/src/core/models/user.g.dart b/packages/stream_chat/lib/src/core/models/user.g.dart index c685cd57..ab2f04d5 100644 --- a/packages/stream_chat/lib/src/core/models/user.g.dart +++ b/packages/stream_chat/lib/src/core/models/user.g.dart @@ -10,7 +10,6 @@ User _$UserFromJson(Map json) { return User( id: json['id'] as String, role: json['role'] as String?, - image: json['image'] as String?, createdAt: json['created_at'] == null ? null : DateTime.parse(json['created_at'] as String), @@ -42,7 +41,6 @@ Map _$UserToJson(User instance) { } writeNotNull('role', readonly(instance.role)); - writeNotNull('image', readonly(instance.image)); writeNotNull('teams', readonly(instance.teams)); writeNotNull('created_at', readonly(instance.createdAt)); writeNotNull('updated_at', readonly(instance.updatedAt)); diff --git a/packages/stream_chat/test/src/core/models/user_test.dart b/packages/stream_chat/test/src/core/models/user_test.dart index 48306017..c8e04ede 100644 --- a/packages/stream_chat/test/src/core/models/user_test.dart +++ b/packages/stream_chat/test/src/core/models/user_test.dart @@ -45,9 +45,9 @@ void main() { final user = User( id: id, role: role, + name: name, image: image, extraData: const { - 'name': name, 'extraDataStringTest': extraDataStringTest, 'extraDataIntTest': extraDataIntTest, 'extraDataDoubleTest': extraDataDoubleTest, @@ -93,9 +93,7 @@ void main() { newUser = user.copyWith( id: 'test', role: 'test', - extraData: { - 'name': 'test', - }, + name: 'test', image: 'https://stream.io/new-image', online: false, banned: false, @@ -120,6 +118,40 @@ void main() { expect(newUser.language, 'it'); }); + test('name property and extraData manipulation', () { + final user = User(id: id, name: name); + + expect(user.name, name); + expect(user.extraData['name'], name); + expect(user.toJson(), {'id': id, 'name': name}); + expect(User.fromJson(user.toJson()).toJson(), {'id': id, 'name': name}); + + const nameOne = 'Name One'; + var newUser = user.copyWith( + extraData: {'name': nameOne}, + ); + + expect(newUser.extraData['name'], nameOne); + expect(newUser.name, nameOne); + + const nameTwo = 'Name Two'; + newUser = user.copyWith( + name: nameTwo, + ); + + expect(newUser.extraData['name'], nameTwo); + expect(newUser.name, nameTwo); + + const nameThree = 'Name Three'; + newUser = user.copyWith( + name: nameThree, + extraData: {'name': nameThree}, + ); + + expect(newUser.extraData['name'], nameThree); + expect(newUser.name, nameThree); + }); + test('image property and extraData manipulation', () { final user = User(id: id, image: image); From 3ab363a8b297a58351c20905d50caec7533faabf Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 5 Aug 2021 16:38:32 +0530 Subject: [PATCH 073/121] chore(llc, ui, core, localization, persistence): analysis fixes, move `extraData.image` to `image` field Signed-off-by: xsahil03x --- packages/stream_chat/example/lib/main.dart | 7 +++---- packages/stream_chat_flutter/example/lib/main.dart | 3 +-- packages/stream_chat_flutter_core/example/lib/main.dart | 9 +++------ .../example/lib/add_new_lang.dart | 3 +-- packages/stream_chat_localizations/example/lib/main.dart | 3 +-- .../example/lib/override_lang.dart | 3 +-- packages/stream_chat_persistence/example/lib/main.dart | 6 ++---- 7 files changed, 12 insertions(+), 22 deletions(-) diff --git a/packages/stream_chat/example/lib/main.dart b/packages/stream_chat/example/lib/main.dart index 46030341..d2c5dcea 100644 --- a/packages/stream_chat/example/lib/main.dart +++ b/packages/stream_chat/example/lib/main.dart @@ -13,10 +13,9 @@ Future main() async { await client.connectUser( User( id: 'cool-shadow-7', - extraData: const { - 'image': - 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow', - }, + name: 'Cool Shadow', + image: + 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow', ), '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9.gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo''', ); diff --git a/packages/stream_chat_flutter/example/lib/main.dart b/packages/stream_chat_flutter/example/lib/main.dart index c2663a6a..a9e1366c 100644 --- a/packages/stream_chat_flutter/example/lib/main.dart +++ b/packages/stream_chat_flutter/example/lib/main.dart @@ -22,8 +22,7 @@ void main() async { /// https://getstream.io/chat/docs/ios_user_setup_and_tokens/ await client.connectUser( User(id: 'super-band-9'), - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.' - '0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A', + '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''', ); final channel = client.channel('messaging', id: 'godevs'); diff --git a/packages/stream_chat_flutter_core/example/lib/main.dart b/packages/stream_chat_flutter_core/example/lib/main.dart index 0a7b0bce..03e791de 100644 --- a/packages/stream_chat_flutter_core/example/lib/main.dart +++ b/packages/stream_chat_flutter_core/example/lib/main.dart @@ -13,13 +13,10 @@ Future main() async { await client.connectUser( User( id: 'cool-shadow-7', - extraData: const { - 'image': - 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow', - }, + image: + 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow', ), - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9' - '.gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo', + '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9.gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo''', ); runApp( diff --git a/packages/stream_chat_localizations/example/lib/add_new_lang.dart b/packages/stream_chat_localizations/example/lib/add_new_lang.dart index 1412f4e0..8a1d196e 100644 --- a/packages/stream_chat_localizations/example/lib/add_new_lang.dart +++ b/packages/stream_chat_localizations/example/lib/add_new_lang.dart @@ -401,8 +401,7 @@ void main() async { /// https://getstream.io/chat/docs/ios_user_setup_and_tokens/ await client.connectUser( User(id: 'super-band-9'), - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.' - '0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A', + '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''', ); final channel = client.channel('messaging', id: 'godevs'); diff --git a/packages/stream_chat_localizations/example/lib/main.dart b/packages/stream_chat_localizations/example/lib/main.dart index 818086df..c99a4c6c 100644 --- a/packages/stream_chat_localizations/example/lib/main.dart +++ b/packages/stream_chat_localizations/example/lib/main.dart @@ -20,8 +20,7 @@ void main() async { /// https://getstream.io/chat/docs/ios_user_setup_and_tokens/ await client.connectUser( User(id: 'super-band-9'), - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.' - '0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A', + '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''', ); final channel = client.channel('messaging', id: 'godevs'); diff --git a/packages/stream_chat_localizations/example/lib/override_lang.dart b/packages/stream_chat_localizations/example/lib/override_lang.dart index 0e45019a..5be8f9a6 100644 --- a/packages/stream_chat_localizations/example/lib/override_lang.dart +++ b/packages/stream_chat_localizations/example/lib/override_lang.dart @@ -45,8 +45,7 @@ void main() async { /// https://getstream.io/chat/docs/ios_user_setup_and_tokens/ await client.connectUser( User(id: 'super-band-9'), - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.' - '0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A', + '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''', ); final channel = client.channel('messaging', id: 'godevs'); diff --git a/packages/stream_chat_persistence/example/lib/main.dart b/packages/stream_chat_persistence/example/lib/main.dart index 5c83ed78..e187da56 100644 --- a/packages/stream_chat_persistence/example/lib/main.dart +++ b/packages/stream_chat_persistence/example/lib/main.dart @@ -22,10 +22,8 @@ Future main() async { await client.connectUser( User( id: 'cool-shadow-7', - extraData: const { - 'image': - 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow', - }, + image: + 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow', ), 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9.' 'gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo', From 22f323b37dc445650d648b0f3353838295e26574 Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Thu, 5 Aug 2021 13:40:00 +0200 Subject: [PATCH 074/121] chore: fix formatting --- .../lib/src/core/models/own_user.dart | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/stream_chat/lib/src/core/models/own_user.dart b/packages/stream_chat/lib/src/core/models/own_user.dart index ee81e909..a65052f4 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.dart @@ -7,11 +7,12 @@ import 'package:stream_chat/stream_chat.dart'; part 'own_user.g.dart'; -/// The class that defines the own user model -/// This object can be found in [Event] +/// The class that defines the own user model. +/// +/// This object can be found in [Event]. @JsonSerializable(createToJson: false) class OwnUser extends User { - /// Constructor used for json serialization + /// Constructor used for json serialization. OwnUser({ this.devices = const [], this.mutes = const [], @@ -45,11 +46,11 @@ class OwnUser extends User { language: language, ); - /// Create a new instance from a json + /// Create a new instance from json. factory OwnUser.fromJson(Map json) => _$OwnUserFromJson( Serializer.moveToExtraDataFromRoot(json, topLevelFields)); - /// Create a new instance from [User] object + /// Create a new instance from [User] object. factory OwnUser.fromUser(User user) => OwnUser( id: user.id, role: user.role, @@ -133,27 +134,28 @@ class OwnUser extends User { ); } - /// List of user devices + /// List of user devices. @JsonKey(includeIfNull: false, defaultValue: []) final List devices; - /// List of users muted by the user + /// List of users muted by the user. @JsonKey(includeIfNull: false, defaultValue: []) final List mutes; - /// List of users muted by the user + /// List of users muted by the user. @JsonKey(includeIfNull: false, defaultValue: []) final List channelMutes; - /// Total unread messages by the user + /// Total unread messages by the user. @JsonKey(includeIfNull: false, defaultValue: 0) final int totalUnreadCount; - /// Total unread channels by the user + /// Total unread channels by the user. @JsonKey(includeIfNull: false) final int? unreadChannels; /// Known top level fields. + /// /// Useful for [Serializer] methods. static final topLevelFields = [ 'devices', From dd3b67a62a589d97836a3af7f625ff5d5c0cb8cf Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Thu, 5 Aug 2021 13:41:32 +0200 Subject: [PATCH 075/121] chore: update CHANGELOG.md --- packages/stream_chat/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index d8860425..0228a0f8 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -4,6 +4,10 @@ - Fixed `channel.markAllRead` throwing failed host lookup. +✅ Added + +- `User` and `OwnUser` classes now have an `image` property. Setting an image will also set the 'image' key on `extraData`, so `user.image` and `user.extraData['image']` is the same. + ## 2.1.1 🐞 Fixed From 35adf26f4cb2c0054276232529527f68a640202f Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Thu, 5 Aug 2021 14:04:35 +0200 Subject: [PATCH 076/121] chore: update CHANGELOG.md --- packages/stream_chat/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 0228a0f8..e3e31db6 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -7,6 +7,7 @@ ✅ Added - `User` and `OwnUser` classes now have an `image` property. Setting an image will also set the 'image' key on `extraData`, so `user.image` and `user.extraData['image']` is the same. +- `User` and `OwnUser` classes now have a `name` property. Setting a name will also set the 'name' key on `extraData`, so `user.name` and `user.extraData['name']` is the same. ## 2.1.1 From 91c0a7031a8f47327b5c6ad8d16b5913b87138f2 Mon Sep 17 00:00:00 2001 From: groovinchip Date: Fri, 6 Aug 2021 11:39:51 -0400 Subject: [PATCH 077/121] feat(ui): added backgroundColor property to the various header widgets Also updated CHANGELOG.md --- packages/stream_chat_flutter/CHANGELOG.md | 1 + packages/stream_chat_flutter/lib/src/channel_header.dart | 6 +++++- .../stream_chat_flutter/lib/src/channel_list_header.dart | 7 ++++++- packages/stream_chat_flutter/lib/src/gallery_header.dart | 7 ++++++- packages/stream_chat_flutter/lib/src/thread_header.dart | 6 +++++- 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 4fe9e6e9..55dfcc62 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -4,6 +4,7 @@ - [#516](https://github.com/GetStream/stream-chat-flutter/issues/516): Added `StreamChatThemeData.placeholderUserImage` for building a widget when the `UserAvatar` image is loading +- Added a `backgroundColor` property to the various header widgets 🔄 Changed diff --git a/packages/stream_chat_flutter/lib/src/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel_header.dart index 11dfc95a..ada04271 100644 --- a/packages/stream_chat_flutter/lib/src/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_header.dart @@ -67,6 +67,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { this.subtitle, this.leading, this.actions, + this.backgroundColor, }) : preferredSize = const Size.fromHeight(kToolbarHeight), super(key: key); @@ -102,6 +103,9 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { /// By default it shows the [ChannelAvatar] final List? actions; + /// The background color for this [ChannelHeader]. + final Color? backgroundColor; + @override Widget build(BuildContext context) { final channel = StreamChannel.of(context).channel; @@ -141,7 +145,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { brightness: Theme.of(context).brightness, elevation: 1, leading: leadingWidget, - backgroundColor: channelHeaderTheme.color, + backgroundColor: backgroundColor ?? channelHeaderTheme.color, actions: actions ?? [ Padding( diff --git a/packages/stream_chat_flutter/lib/src/channel_list_header.dart b/packages/stream_chat_flutter/lib/src/channel_list_header.dart index 692db477..4ce8b0d2 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_header.dart @@ -61,6 +61,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { this.subtitle, this.leading, this.actions, + this.backgroundColor, }) : super(key: key); /// Pass this if you don't have a [StreamChatClient] in your widget tree. @@ -93,6 +94,9 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { /// By default it shows the new chat button final List? actions; + /// The background color for this [ChannelListHeader]. + final Color? backgroundColor; + @override Widget build(BuildContext context) { final _client = client ?? StreamChat.of(context).client; @@ -124,7 +128,8 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { textTheme: Theme.of(context).textTheme, brightness: Theme.of(context).brightness, elevation: 1, - backgroundColor: channelListHeaderThemeData.color, + backgroundColor: + backgroundColor ?? channelListHeaderThemeData.color, centerTitle: true, leading: leading ?? Center( diff --git a/packages/stream_chat_flutter/lib/src/gallery_header.dart b/packages/stream_chat_flutter/lib/src/gallery_header.dart index 3e732898..74eeb4ee 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_header.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_header.dart @@ -19,6 +19,7 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { this.onImageTap, this.userName = '', this.sentAt = '', + this.backgroundColor, }) : preferredSize = const Size.fromHeight(kToolbarHeight), super(key: key); @@ -50,6 +51,9 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { /// Stores the current index of media shown final int currentIndex; + /// The background color of this [GalleryHeader]. + final Color? backgroundColor; + @override Widget build(BuildContext context) { final galleryHeaderThemeData = GalleryHeaderTheme.of(context); @@ -66,7 +70,8 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { onPressed: onBackPressed, ) : const SizedBox(), - backgroundColor: galleryHeaderThemeData.backgroundColor, + backgroundColor: + backgroundColor ?? galleryHeaderThemeData.backgroundColor, actions: [ if (!message.isEphemeral) IconButton( diff --git a/packages/stream_chat_flutter/lib/src/thread_header.dart b/packages/stream_chat_flutter/lib/src/thread_header.dart index 66b6b8be..abbd9bf0 100644 --- a/packages/stream_chat_flutter/lib/src/thread_header.dart +++ b/packages/stream_chat_flutter/lib/src/thread_header.dart @@ -70,6 +70,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { this.actions, this.onTitleTap, this.showTypingIndicator = true, + this.backgroundColor, }) : preferredSize = const Size.fromHeight(kToolbarHeight), super(key: key); @@ -102,6 +103,9 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { /// if a user is typing in this thread final bool showTypingIndicator; + /// The background color of this [ThreadHeader]. + final Color? backgroundColor; + @override Widget build(BuildContext context) { final channelHeaderTheme = ChannelHeaderTheme.of(context); @@ -136,7 +140,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { showUnreads: true, ) : const SizedBox()), - backgroundColor: channelHeaderTheme.color, + backgroundColor: backgroundColor ?? channelHeaderTheme.color, centerTitle: true, actions: actions, title: InkWell( From b8de269c1f88985b7262d3b7b27a411800bd76bf Mon Sep 17 00:00:00 2001 From: groovinchip Date: Fri, 6 Aug 2021 12:16:22 -0400 Subject: [PATCH 078/121] chore: updated CHANGELOG.md --- packages/stream_chat_flutter/CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 55dfcc62..cbcad168 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -4,7 +4,11 @@ - [#516](https://github.com/GetStream/stream-chat-flutter/issues/516): Added `StreamChatThemeData.placeholderUserImage` for building a widget when the `UserAvatar` image is loading -- Added a `backgroundColor` property to the various header widgets +- Added a `backgroundColor` property to the following widgets: + - `ChannelHeader` + - `ChannelListHeader` + - `GalleryHeader` + - `ThreadHeader` 🔄 Changed From bdc527f8d312418563adbd84633a05b05a8e4bc7 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Sat, 7 Aug 2021 00:58:46 +0530 Subject: [PATCH 079/121] feat(ui): added backgroundColor property to GalleryFooter Signed-off-by: xsahil03x --- packages/stream_chat_flutter/lib/src/gallery_footer.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/gallery_footer.dart b/packages/stream_chat_flutter/lib/src/gallery_footer.dart index 74a7ac45..365368ac 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_footer.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_footer.dart @@ -27,6 +27,7 @@ class GalleryFooter extends StatefulWidget implements PreferredSizeWidget { this.totalPages = 0, this.mediaAttachments = const [], this.mediaSelectedCallBack, + this.backgroundColor, }) : preferredSize = const Size.fromHeight(kToolbarHeight), super(key: key); @@ -55,6 +56,9 @@ class GalleryFooter extends StatefulWidget implements PreferredSizeWidget { /// Callback when media is selected final ValueChanged? mediaSelectedCallBack; + /// The background color of this [GalleryFooter]. + final Color? backgroundColor; + @override _GalleryFooterState createState() => _GalleryFooterState(); @@ -90,7 +94,8 @@ class _GalleryFooterState extends State { context: context, removeTop: true, child: BottomAppBar( - color: galleryFooterThemeData.backgroundColor, + color: + widget.backgroundColor ?? galleryFooterThemeData.backgroundColor, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ From f30704dbeb8b489a5eb2d1abe3c0867a577e1004 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Sat, 7 Aug 2021 00:59:02 +0530 Subject: [PATCH 080/121] chore(ui): update CHANGELOG.md Signed-off-by: xsahil03x --- packages/stream_chat_flutter/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index cbcad168..5289ec30 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -8,6 +8,7 @@ - `ChannelHeader` - `ChannelListHeader` - `GalleryHeader` + - `GalleryFooter` - `ThreadHeader` From 2c0eaaafab89179e2f8ed94468672cfa1876310e Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Sun, 8 Aug 2021 22:27:40 +0530 Subject: [PATCH 081/121] added new guide --- .../docs/Flutter/guides/understanding_filters.mdx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docusaurus/docs/Flutter/guides/understanding_filters.mdx diff --git a/docusaurus/docs/Flutter/guides/understanding_filters.mdx b/docusaurus/docs/Flutter/guides/understanding_filters.mdx new file mode 100644 index 00000000..47d72e1f --- /dev/null +++ b/docusaurus/docs/Flutter/guides/understanding_filters.mdx @@ -0,0 +1,15 @@ +--- +id: understanding_filters +sidebar_position: 10 +title: Understanding Filters +--- + +Understanding Filters + +### Introduction + +Filters are used to get a specific subset of objects (channels, users, messages, members, etc) which +fit the conditions specified. Earlier versions of the SDK contained String-based filters which are now replaced by type-safe +filters. This guide aims to explain the different types of filters and how to use them. + + From ff4baabcad05c9b75deae446fef636e24b731673 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 9 Aug 2021 16:38:02 +0530 Subject: [PATCH 082/121] added guides --- .../Flutter/guides/understanding_filters.mdx | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/docusaurus/docs/Flutter/guides/understanding_filters.mdx b/docusaurus/docs/Flutter/guides/understanding_filters.mdx index 47d72e1f..8a6fc925 100644 --- a/docusaurus/docs/Flutter/guides/understanding_filters.mdx +++ b/docusaurus/docs/Flutter/guides/understanding_filters.mdx @@ -12,4 +12,132 @@ Filters are used to get a specific subset of objects (channels, users, messages, fit the conditions specified. Earlier versions of the SDK contained String-based filters which are now replaced by type-safe filters. This guide aims to explain the different types of filters and how to use them. +### Types Of Filters +#### Filter.equal + +The 'equal' filter gets the objects where the given key has the specified value. + +```dart +Filter.equal('type', 'messaging'), +``` + +#### Filter.notEqual + +The 'notEqual' filter gets the objects where the given key does not have the specified value. + +```dart +Filter.notEqual('type', 'messaging'), +``` + +#### Filter.greater + +The 'greater' filter gets the objects where the given key has a higher value than the specified value. + +```dart +Filter.greater('count', 5), +``` + +#### Filter.greaterOrEqual + +The 'greaterOrEqual' filter gets the objects where the given key has an equal or higher value than the specified value. + +```dart +Filter.greaterOrEqual('count', 5), +``` + +#### Filter.less + +The 'less' filter gets the objects where the given key has a lesser value than the specified value. + +```dart +Filter.less('count', 5), +``` + +#### Filter.lessOrEqual + +The 'lessOrEqual' filter gets the objects where the given key has a lesser or equal value than the specified value. + +```dart +Filter.lessOrEqual('count', 5), +``` + +#### Filter.in_ + +The 'in_' filter allows getting objects where the key matches any in a specified array. + +```dart +Filter.in_('members', [user.id]) +``` + +:::note +Since 'in' is a keyword in Dart, the filter has an underscore added. This does not apply to the 'notIn' +keyword. +::: + +#### Filter.notIn + +The 'notIn' filter allows getting objects where the key matches none in a specified array. + +```dart +Filter.notIn('members', [user.id]) +``` + +#### Filter.query + +Matches values by performing text search with the specified value. + +```dart +Filter.query('name', 'demo') +``` + +#### Filter.autoComplete + +Matches values with the specified prefix. + +```dart +Filter.autoComplete('name', 'demo') +``` + +#### Filter.exists + +Matches values that exist/don't exist based on the specified boolean value. + +```dart +Filter.exists('name', true) +``` + +### Group Queries + +#### FilterOperator.and + +The 'and' operator combines multiple queries. + +```dart +final filter = Filter.and([ + Filter.equal('type', 'messaging'), + Filter.in_('members', [user.id]) +]) +``` + +#### FilterOperator.or + +Combines the provided filters and matches the values matched by at least one of the filters. + +```dart +final filter = Filter.or([ + Filter.in_('bannedUsers', [user.id]), + Filter.in_('shadowBannedUsers', [user.id]) +]) +``` + +#### FilterOperator.nor + +Combines the provided filters and matches the values not matched by all the filters. + +```dart +final filter = Filter.nor([ + Filter.in_('bannedUsers', [user.id]), + Filter.in_('shadowBannedUsers', [user.id]) +]) +``` From f8a6539d09fb364f345f88750d95a45a2cb2ab7e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 9 Aug 2021 16:48:23 +0530 Subject: [PATCH 083/121] feat(ui): add support for `attachmentButtonBuilder`, `commandButtonBuilder` in `MessageInput` Signed-off-by: xsahil03x --- packages/stream_chat_flutter/CHANGELOG.md | 144 +++++++++++------- .../lib/src/extension.dart | 51 ++++++- .../lib/src/message_input.dart | 104 ++++++++----- .../lib/stream_chat_flutter.dart | 1 + 4 files changed, 198 insertions(+), 102 deletions(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 5289ec30..341d481b 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -2,21 +2,35 @@ ✅ Added -- [#516](https://github.com/GetStream/stream-chat-flutter/issues/516): Added `StreamChatThemeData.placeholderUserImage` for - building a widget when the `UserAvatar` image is loading +- [#516](https://github.com/GetStream/stream-chat-flutter/issues/516): + Added `StreamChatThemeData.placeholderUserImage` for building a widget when the `UserAvatar` image + is loading - Added a `backgroundColor` property to the following widgets: - - `ChannelHeader` - - `ChannelListHeader` - - `GalleryHeader` - - `GalleryFooter` - - `ThreadHeader` + - `ChannelHeader` + - `ChannelListHeader` + - `GalleryHeader` + - `GalleryFooter` + - `ThreadHeader` +- Added `MessageInput.attachmentButtonBuilder` and `MessageInput.commandButtonBuilder` for more + customizations. + +```dart +typedef ActionButtonBuilder = Widget Function( + BuildContext context, + IconButton defaultActionButton, + ); +``` + +> **_NOTE:_** The last parameter is the default `ActionButton` +You can call `.copyWith` to customize just a subset of properties. 🔄 Changed -Theming has been upgraded! Most theme classes now have `InheritedTheme` classes -associated with them, and have been upgraded with some goodies like `lerp` functions. -Here's the full naming breakdown: +Theming has been upgraded! Most theme classes now have `InheritedTheme` classes associated with +them, and have been upgraded with some goodies like `lerp` functions. Here's the full naming +breakdown: + * `AvatarTheme` is now `AvatarThemeData` * `ChannelHeaderTheme` is now `ChannelHeaderThemeData` * `ChannelListHeaderTheme` is now `ChannelListHeaderThemeData` @@ -32,8 +46,9 @@ Here's the full naming breakdown: 🐞 Fixed -- [#590](https://github.com/GetStream/stream-chat-flutter/issues/590): livestream use case, no members when sending message - +- [#590](https://github.com/GetStream/stream-chat-flutter/issues/590): livestream use case, no + members when sending message + ## 2.1.1 - Updated core dependency @@ -50,7 +65,8 @@ Here's the full naming breakdown: 🔄 Changed - `StreamChat.of(context).user` is now deprecated in favor of `StreamChat.of(context).currentUser`. -- `StreamChat.of(context).userStream` is now deprecated in favor of `StreamChat.of(context).currentUserStream`. +- `StreamChat.of(context).userStream` is now deprecated in favor + of `StreamChat.of(context).currentUserStream`. 🐞 Fixed @@ -64,17 +80,17 @@ Here's the full naming breakdown: - Renamed `ChannelImage` to `ChannelAvatar` - Updated `StreamChatThemeData.reactionIcons` to accept custom builder - Renamed `ColorTheme` properties to reflect the purpose of the colors - - `ColorTheme.black` -> `ColorTheme.textHighEmphasis` - - `ColorTheme.grey` -> `ColorTheme.textLowEmphasis` - - `ColorTheme.greyGainsboro` -> `ColorTheme.disabled` - - `ColorTheme.greyWhisper` -> `ColorTheme.borders` - - `ColorTheme.whiteSmoke` -> `ColorTheme.inputBg` - - `ColorTheme.whiteSnow` -> `ColorTheme.appBg` - - `ColorTheme.white` -> `ColorTheme.barsBg` - - `ColorTheme.blueAlice` -> `ColorTheme.linkBg` - - `ColorTheme.accentBlue` -> `ColorTheme.accentPrimary` - - `ColorTheme.accentRed` -> `ColorTheme.accentError` - - `ColorTheme.accentGreen` -> `ColorTheme.accentInfo` + - `ColorTheme.black` -> `ColorTheme.textHighEmphasis` + - `ColorTheme.grey` -> `ColorTheme.textLowEmphasis` + - `ColorTheme.greyGainsboro` -> `ColorTheme.disabled` + - `ColorTheme.greyWhisper` -> `ColorTheme.borders` + - `ColorTheme.whiteSmoke` -> `ColorTheme.inputBg` + - `ColorTheme.whiteSnow` -> `ColorTheme.appBg` + - `ColorTheme.white` -> `ColorTheme.barsBg` + - `ColorTheme.blueAlice` -> `ColorTheme.linkBg` + - `ColorTheme.accentBlue` -> `ColorTheme.accentPrimary` + - `ColorTheme.accentRed` -> `ColorTheme.accentError` + - `ColorTheme.accentGreen` -> `ColorTheme.accentInfo` - `ChannelListCore` options property is removed in favor of individual properties - `options.state` -> bool state @@ -95,7 +111,7 @@ typedef MessageBuilder = Widget Function( ); ``` -the last parameter is the default `MessageWidget` +> **_NOTE:_** the last parameter is the default `MessageWidget` You can call `.copyWith` to customize just a subset of properties @@ -103,7 +119,8 @@ You can call `.copyWith` to customize just a subset of properties - Added video compress options (frame and quality) to `MessageInput` - TypingIndicator now has a property called `parentId` to show typing indicator specific to threads -- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView header/footer +- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView + header/footer - `MessageWidget` accepts a `userAvatarBuilder` - Added pinMessage ui support - Added `MessageListView.threadSeparatorBuilder` property @@ -112,10 +129,12 @@ You can call `.copyWith` to customize just a subset of properties 🐞 Fixed -- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing - message -- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case -- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without a reload +- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text + box when editing message +- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator + use case +- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without + a reload - `MessageListView` not rendering if the user is not a member of the channel - Fix `MessageInput` overflow when there are no actions - Minor fixes and improvements @@ -125,17 +144,17 @@ You can call `.copyWith` to customize just a subset of properties 🛑️ Breaking Changes from `2.0.0-nullsafety.8` - Renamed `ColorTheme` properties to reflect the purpose of the colors - - `ColorTheme.black` -> `ColorTheme.textHighEmphasis` - - `ColorTheme.grey` -> `ColorTheme.textLowEmphasis` - - `ColorTheme.greyGainsboro` -> `ColorTheme.disabled` - - `ColorTheme.greyWhisper` -> `ColorTheme.borders` - - `ColorTheme.whiteSmoke` -> `ColorTheme.inputBg` - - `ColorTheme.whiteSnow` -> `ColorTheme.appBg` - - `ColorTheme.white` -> `ColorTheme.barsBg` - - `ColorTheme.blueAlice` -> `ColorTheme.linkBg` - - `ColorTheme.accentBlue` -> `ColorTheme.accentPrimary` - - `ColorTheme.accentRed` -> `ColorTheme.accentError` - - `ColorTheme.accentGreen` -> `ColorTheme.accentInfo` + - `ColorTheme.black` -> `ColorTheme.textHighEmphasis` + - `ColorTheme.grey` -> `ColorTheme.textLowEmphasis` + - `ColorTheme.greyGainsboro` -> `ColorTheme.disabled` + - `ColorTheme.greyWhisper` -> `ColorTheme.borders` + - `ColorTheme.whiteSmoke` -> `ColorTheme.inputBg` + - `ColorTheme.whiteSnow` -> `ColorTheme.appBg` + - `ColorTheme.white` -> `ColorTheme.barsBg` + - `ColorTheme.blueAlice` -> `ColorTheme.linkBg` + - `ColorTheme.accentBlue` -> `ColorTheme.accentPrimary` + - `ColorTheme.accentRed` -> `ColorTheme.accentError` + - `ColorTheme.accentGreen` -> `ColorTheme.accentInfo` ✅ Added @@ -162,21 +181,24 @@ typedef MessageBuilder = Widget Function( ); ``` -the last parameter is the default `MessageWidget` -You can call `.copyWith` to customize just a subset of properties +> **_NOTE:_** The last parameter is the default `MessageWidget` +You can call `.copyWith` to customize just a subset of properties. ✅ Added - TypingIndicator now has a property called `parentId` to show typing indicator specific to threads -- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView header/footer +- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView + header/footer - `MessageWidget` accepts a `userAvatarBuilder` 🐞 Fixed -- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing - message -- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case -- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without a reload +- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text + box when editing message +- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator + use case +- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without + a reload - `MessageListView` not rendering if the user is not a member of the channel ## 2.0.0-nullsafety.7 @@ -246,7 +268,8 @@ You can call `.copyWith` to customize just a subset of properties - Show error messages as system and keep them in the message input - Remove notification badge logic - Use shimmer while loading images -- Polished `StreamChatTheme` adding more options and a new `MessageInputTheme` dedicated to `MessageInput` +- Polished `StreamChatTheme` adding more options and a new `MessageInputTheme` dedicated + to `MessageInput` - Add possibility to specify custom message actions using `MessageWidget.customActions` - Added `MessageListView.onAttachmentTap` callback - Fixed message newline issue @@ -303,7 +326,8 @@ You can call `.copyWith` to customize just a subset of properties - Improved api documentation - Updated `stream_chat` dependency to `^1.0.0-beta` - Extracted sample app into dedicated [repo](https://github.com/GetStream/flutter-samples) -- Reimplemented existing widgets using [stream_chat_flutter_core](https://pub.dev/packages/stream_chat_flutter_core) +- Reimplemented existing widgets + using [stream_chat_flutter_core](https://pub.dev/packages/stream_chat_flutter_core) ## 0.2.21 @@ -320,8 +344,8 @@ You can call `.copyWith` to customize just a subset of properties ## 0.2.20+2 -- Added `shouldAddChannel` to ChannelsBloc in order to check if a channel has to be added to the list when a new message - arrives +- Added `shouldAddChannel` to ChannelsBloc in order to check if a channel has to be added to the + list when a new message arrives ## 0.2.20+1 @@ -355,7 +379,8 @@ You can call `.copyWith` to customize just a subset of properties ## 0.2.16 -- Do not wrap channel preview builder. Users will have to implement they're custom onTap/onLongPress implementation +- Do not wrap channel preview builder. Users will have to implement they're custom onTap/onLongPress + implementation - Make public autofocus field of the TextField of message_input ## 0.2.15 @@ -540,10 +565,11 @@ You can call `.copyWith` to customize just a subset of properties ## 0.2.1-alpha+1 -- Removed the additional `Navigator` in `StreamChat` widget. It was added to make the app have the `StreamChat` widget - as ancestor in every route. Now the recommended way to add `StreamChat` to your app is using the `builder` property of - your `MaterialApp` widget. Otherwise you can use it in the usual way, but you need to add a `StreamChat` widget to - every route of your app. Read [this issue](https://github.com/GetStream/stream-chat-flutter/issues/47) for more +- Removed the additional `Navigator` in `StreamChat` widget. It was added to make the app have + the `StreamChat` widget as ancestor in every route. Now the recommended way to add `StreamChat` to + your app is using the `builder` property of your `MaterialApp` widget. Otherwise you can use it in + the usual way, but you need to add a `StreamChat` widget to every route of your app. + Read [this issue](https://github.com/GetStream/stream-chat-flutter/issues/47) for more information. ```dart @@ -645,8 +671,8 @@ Widget build(BuildContext context) { - Add gesture (vertical drag down) to close the keyboard -- Add keyboard type parameters (set it to TextInputType.text to show the submit button that will even close the - keyboard) +- Add keyboard type parameters (set it to TextInputType.text to show the submit button that will + even close the keyboard) The property showVideoFullScreen was added mainly because of this issue brianegan/chewie#261 diff --git a/packages/stream_chat_flutter/lib/src/extension.dart b/packages/stream_chat_flutter/lib/src/extension.dart index b975941e..2780e1c8 100644 --- a/packages/stream_chat_flutter/lib/src/extension.dart +++ b/packages/stream_chat_flutter/lib/src/extension.dart @@ -46,9 +46,9 @@ extension PlatformFileX on PlatformFile { ); } -/// +/// Extension on [InputDecoration] extension InputDecorationX on InputDecoration { - /// + /// Merges this [AvatarThemeData] with the [other] InputDecoration merge(InputDecoration? other) { if (other == null) return this; return copyWith( @@ -123,3 +123,50 @@ extension FlipBorder on BorderRadius { bottomRight: bottomLeft) : this; } + +/// Extension on [IconButton] +extension IconButtonX on IconButton { + /// Creates a copy of [IconButton] with specified attributes overridden. + IconButton copyWith({ + double? iconSize, + VisualDensity? visualDensity, + EdgeInsetsGeometry? padding, + AlignmentGeometry? alignment, + double? splashRadius, + Color? color, + Color? focusColor, + Color? hoverColor, + Color? highlightColor, + Color? splashColor, + Color? disabledColor, + void Function()? onPressed, + MouseCursor? mouseCursor, + FocusNode? focusNode, + bool? autofocus, + String? tooltip, + bool? enableFeedback, + BoxConstraints? constraints, + Widget? icon, + }) => + IconButton( + iconSize: iconSize ?? this.iconSize, + visualDensity: visualDensity ?? this.visualDensity, + padding: padding ?? this.padding, + alignment: alignment ?? this.alignment, + splashRadius: splashRadius ?? this.splashRadius, + color: color ?? this.color, + focusColor: focusColor ?? this.focusColor, + hoverColor: hoverColor ?? this.hoverColor, + highlightColor: highlightColor ?? this.highlightColor, + splashColor: splashColor ?? this.splashColor, + disabledColor: disabledColor ?? this.disabledColor, + onPressed: onPressed ?? this.onPressed, + mouseCursor: mouseCursor ?? this.mouseCursor, + focusNode: focusNode ?? this.focusNode, + autofocus: autofocus ?? this.autofocus, + tooltip: tooltip ?? this.tooltip, + enableFeedback: enableFeedback ?? this.enableFeedback, + constraints: constraints ?? this.constraints, + icon: icon ?? this.icon, + ); +} diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 9431cd22..f5e4158a 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -50,6 +50,14 @@ typedef MentionTileBuilder = Widget Function( Member member, ); +/// Widget builder for action button +/// [defaultActionButton] is the default [IconButton] configuration +/// Use [defaultActionButton.copyWith] to easily customize it +typedef ActionButtonBuilder = Widget Function( + BuildContext context, + IconButton defaultActionButton, +); + /// Location for actions on the [MessageInput] enum ActionsLocation { /// Align to left @@ -164,6 +172,8 @@ class MessageInput extends StatefulWidget { this.compressedVideoQuality = VideoQuality.DefaultQuality, this.compressedVideoFrameRate = 30, this.onError, + this.attachmentButtonBuilder, + this.commandButtonBuilder, }) : super(key: key); /// Message to edit @@ -247,6 +257,12 @@ class MessageInput extends StatefulWidget { /// A callback for error reporting final ErrorListener? onError; + /// Builder for customizing attachment button. + final ActionButtonBuilder? attachmentButtonBuilder; + + /// Builder for customizing command button. + final ActionButtonBuilder? commandButtonBuilder; + @override MessageInputState createState() => MessageInputState(); @@ -403,11 +419,11 @@ class MessageInputState extends State { children: [ if (!_commandEnabled && widget.actionsLocation == ActionsLocation.left) - _buildExpandActionsButton(), + _buildExpandActionsButton(context), _buildTextInput(context), if (!_commandEnabled && widget.actionsLocation == ActionsLocation.right) - _buildExpandActionsButton(), + _buildExpandActionsButton(context), if (widget.sendButtonLocation == SendButtonLocation.outside) _animateSendButton(context), ], @@ -490,7 +506,7 @@ class MessageInputState extends State { ); } - Widget _buildExpandActionsButton() { + Widget _buildExpandActionsButton(BuildContext context) { final channel = StreamChannel.of(context).channel; return Padding( padding: const EdgeInsets.symmetric(horizontal: 8), @@ -528,12 +544,13 @@ class MessageInputState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - if (!widget.disableAttachments) _buildAttachmentButton(), + if (!widget.disableAttachments) + _buildAttachmentButton(context), if (widget.showCommandsButton && widget.editMessage == null && channel.state != null && channel.config?.commands.isNotEmpty == true) - _buildCommandButton(), + _buildCommandButton(context), ...widget.actions ?? [], ].insertBetween(const SizedBox(width: 8)), ), @@ -669,9 +686,7 @@ class MessageInputState extends State { : (widget.actionsLocation == ActionsLocation.leftInside ? Row( mainAxisSize: MainAxisSize.min, - children: [ - _buildExpandActionsButton(), - ], + children: [_buildExpandActionsButton(context)], ) : null), suffixIconConstraints: const BoxConstraints.tightFor(height: 40), @@ -697,7 +712,7 @@ class MessageInputState extends State { ), if (!_commandEnabled && widget.actionsLocation == ActionsLocation.rightInside) - _buildExpandActionsButton(), + _buildExpandActionsButton(context), if (widget.sendButtonLocation == SendButtonLocation.inside) _animateSendButton(context), ], @@ -1681,10 +1696,9 @@ class MessageInputState extends State { } } - Widget _buildCommandButton() { + Widget _buildCommandButton(BuildContext context) { final s = textEditingController.text.trim(); - - return IconButton( + final defaultButton = IconButton( icon: StreamSvgIcon.lightning( color: s.isNotEmpty ? _streamChatTheme.colorTheme.disabled @@ -1722,38 +1736,46 @@ class MessageInputState extends State { } }, ); + + return widget.commandButtonBuilder?.call(context, defaultButton) ?? + defaultButton; } - Widget _buildAttachmentButton() => IconButton( - icon: StreamSvgIcon.attach( - color: _openFilePickerSection - ? _messageInputTheme.actionButtonColor - : _messageInputTheme.actionButtonIdleColor, - ), - padding: const EdgeInsets.all(0), - constraints: const BoxConstraints.tightFor( - height: 24, - width: 24, - ), - splashRadius: 24, - onPressed: () async { - _emojiOverlay?.remove(); - _emojiOverlay = null; - _commandsOverlay?.remove(); - _commandsOverlay = null; - _mentionsOverlay?.remove(); - _mentionsOverlay = null; + Widget _buildAttachmentButton(BuildContext context) { + final defaultButton = IconButton( + icon: StreamSvgIcon.attach( + color: _openFilePickerSection + ? _messageInputTheme.actionButtonColor + : _messageInputTheme.actionButtonIdleColor, + ), + padding: const EdgeInsets.all(0), + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, + ), + splashRadius: 24, + onPressed: () async { + _emojiOverlay?.remove(); + _emojiOverlay = null; + _commandsOverlay?.remove(); + _commandsOverlay = null; + _mentionsOverlay?.remove(); + _mentionsOverlay = null; - if (_openFilePickerSection) { - setState(() { - _openFilePickerSection = false; - _filePickerSize = _kMinMediaPickerSize; - }); - } else { - showAttachmentModal(); - } - }, - ); + if (_openFilePickerSection) { + setState(() { + _openFilePickerSection = false; + _filePickerSize = _kMinMediaPickerSize; + }); + } else { + showAttachmentModal(); + } + }, + ); + + return widget.attachmentButtonBuilder?.call(context, defaultButton) ?? + defaultButton; + } /// Show the attachment modal, making the user choose where to /// pick a media from diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 89c6450c..42188fa0 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -12,6 +12,7 @@ export 'src/channel_preview.dart'; export 'src/connection_status_builder.dart'; export 'src/date_divider.dart'; export 'src/deleted_message.dart'; +export 'src/extension.dart' show IconButtonX; export 'src/full_screen_media.dart'; export 'src/gallery_footer.dart'; export 'src/gallery_header.dart'; From aa223ce25877d9c9ba5aec6f247f27a40f0a7f3b Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Mon, 9 Aug 2021 14:30:07 +0200 Subject: [PATCH 084/121] chore: format comments --- .../stream_chat/lib/src/client/channel.dart | 232 ++++++++++-------- 1 file changed, 123 insertions(+), 109 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index a05587ab..4d4a3858 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -63,145 +63,145 @@ class Channel { _extraData.addAll(extraData); } - /// Returns true if the channel is muted + /// Returns true if the channel is muted. bool get isMuted => _client.state.currentUser?.channelMutes .any((element) => element.channel.cid == cid) == true; - /// Returns true if the channel is muted as a stream + /// Returns true if the channel is muted, as a stream. Stream? get isMutedStream => _client.state.currentUserStream .map((event) => event!.channelMutes.any((element) => element.channel.cid == cid) == true) .distinct(); - /// True if the channel is a group + /// True if the channel is a group. bool get isGroup => memberCount != 2; - /// True if the channel is distinct + /// True if the channel is distinct. bool get isDistinct => id?.startsWith('!members') == true; - /// Channel configuration + /// Channel configuration. ChannelConfig? get config { _checkInitialized(); return state?._channelState.channel?.config; } - /// Channel configuration as a stream + /// Channel configuration as a stream. Stream? get configStream { _checkInitialized(); return state?.channelStateStream.map((cs) => cs.channel?.config); } - /// Channel user creator + /// Channel user creator. User? get createdBy { _checkInitialized(); return state?._channelState.channel?.createdBy; } - /// Channel user creator as a stream + /// Channel user creator as a stream. Stream? get createdByStream { _checkInitialized(); return state?.channelStateStream.map((cs) => cs.channel?.createdBy); } - /// Channel frozen status + /// Channel frozen status. bool? get frozen { _checkInitialized(); return state?._channelState.channel?.frozen; } - /// Channel frozen status as a stream + /// Channel frozen status as a stream. Stream? get frozenStream { _checkInitialized(); return state?.channelStateStream.map((cs) => cs.channel?.frozen); } - /// Channel creation date + /// Channel creation date. DateTime? get createdAt { _checkInitialized(); return state?._channelState.channel?.createdAt; } - /// Channel creation date as a stream + /// Channel creation date as a stream. Stream? get createdAtStream { _checkInitialized(); return state?.channelStateStream.map((cs) => cs.channel?.createdAt); } - /// Channel last message date + /// Channel last message date. DateTime? get lastMessageAt { _checkInitialized(); return state?._channelState.channel?.lastMessageAt; } - /// Channel last message date as a stream + /// Channel last message date as a stream. Stream? get lastMessageAtStream { _checkInitialized(); return state?.channelStateStream.map((cs) => cs.channel?.lastMessageAt); } - /// Channel updated date + /// Channel updated date. DateTime? get updatedAt { _checkInitialized(); return state?._channelState.channel?.updatedAt; } - /// Channel updated date as a stream + /// Channel updated date as a stream. Stream? get updatedAtStream { _checkInitialized(); return state?.channelStateStream.map((cs) => cs.channel?.updatedAt); } - /// Channel deletion date + /// Channel deletion date. DateTime? get deletedAt { _checkInitialized(); return state?._channelState.channel?.deletedAt; } - /// Channel deletion date as a stream + /// Channel deletion date as a stream. Stream? get deletedAtStream { _checkInitialized(); return state?.channelStateStream.map((cs) => cs.channel?.deletedAt); } - /// Channel member count + /// Channel member count. int? get memberCount { _checkInitialized(); return state?._channelState.channel?.memberCount; } - /// Channel member count as a stream + /// Channel member count as a stream. Stream? get memberCountStream { _checkInitialized(); return state?.channelStateStream.map((cs) => cs.channel?.memberCount); } - /// Channel id + /// Channel id. String? get id => state?._channelState.channel?.id ?? _id; - /// Channel type + /// Channel type. String get type => state?._channelState.channel?.type ?? _type; - /// Channel cid + /// Channel cid. String? get cid => state?._channelState.channel?.cid ?? _cid; - /// Channel team + /// Channel team. String? get team { _checkInitialized(); return state?._channelState.channel?.team; } - /// Channel extra data + /// Channel extra data. Map get extraData { var data = state?._channelState.channel?.extraData; if (data == null || data.isEmpty) { @@ -210,7 +210,7 @@ class Channel { return data; } - /// Channel extra data as a stream + /// Channel extra data as a stream. Stream> get extraDataStream { _checkInitialized(); return state!.channelStateStream.map( @@ -224,9 +224,10 @@ class Channel { final Completer _initializedCompleter = Completer(); - /// True if this is initialized + /// True if this is initialized. + /// /// Call [watch] to initialize the client or instantiate it using - /// [Channel.fromState] + /// [Channel.fromState]. Future get initialized => _initializedCompleter.future; final _cancelableAttachmentUploadRequest = {}; @@ -362,7 +363,9 @@ class Channel { } /// Send a [message] to this channel. - /// If [skipPush] is true the message will not send a push notification + /// + /// If [skipPush] is true the message will not send a push notification. + /// /// Waits for a [_messageAttachmentsUploadCompleter] to complete /// before actually sending the message. Future sendMessage( @@ -427,6 +430,7 @@ class Channel { } /// Updates the [message] in this channel. + /// /// Waits for a [_messageAttachmentsUploadCompleter] to complete /// before actually updating the message. Future updateMessage(Message message) async { @@ -489,8 +493,10 @@ class Channel { } /// Partially updates the [message] in this channel. - /// Use [set] to define values to be set - /// Use [unset] to define values to be unset + /// + /// Use [set] to define values to be set. + /// + /// Use [unset] to define values to be unset. Future partialUpdateMessage( Message message, { Map? set, @@ -590,7 +596,7 @@ class Channel { ); } - /// Unpins provided message + /// Unpins provided message. Future unpinMessage(Message message) => partialUpdateMessage( message, @@ -599,7 +605,7 @@ class Channel { }, ); - /// Send a file to this channel + /// Send a file to this channel. Future sendFile( AttachmentFile file, { ProgressCallback? onSendProgress, @@ -615,7 +621,7 @@ class Channel { ); } - /// Send an image to this channel + /// Send an image to this channel. Future sendImage( AttachmentFile file, { ProgressCallback? onSendProgress, @@ -631,7 +637,7 @@ class Channel { ); } - /// A message search. + /// Search for a message with the given options. Future search({ String? query, Filter? messageFilters, @@ -648,7 +654,7 @@ class Channel { ); } - /// Delete a file from this channel + /// Delete a file from this channel. Future deleteFile( String url, { CancelToken? cancelToken, @@ -662,7 +668,7 @@ class Channel { ); } - /// Delete an image from this channel + /// Delete an image from this channel. Future deleteImage( String url, { CancelToken? cancelToken, @@ -676,14 +682,15 @@ class Channel { ); } - /// Send an event on this channel + /// Send an event on this channel. Future sendEvent(Event event) { _checkInitialized(); return _client.sendEvent(id!, type, event); } - /// Send a reaction to this channel - /// Set [enforceUnique] to true to remove the existing user reaction + /// Send a reaction to this channel. + /// + /// Set [enforceUnique] to true to remove the existing user reaction. Future sendReaction( Message message, String type, { @@ -746,7 +753,7 @@ class Channel { } } - /// Delete a reaction from this channel + /// Delete a reaction from this channel. Future deleteReaction( Message message, Reaction reaction) async { final type = reaction.type; @@ -821,25 +828,25 @@ class Channel { return _client.deleteChannel(id!, type); } - /// Removes all messages from the channel + /// Removes all messages from the channel. Future truncate() async { _checkInitialized(); return _client.truncateChannel(id!, type); } - /// Accept invitation to the channel + /// Accept invitation to the channel. Future acceptInvite([Message? message]) async { _checkInitialized(); return _client.acceptChannelInvite(id!, type, message: message); } - /// Reject invitation to the channel + /// Reject invitation to the channel. Future rejectInvite([Message? message]) async { _checkInitialized(); return _client.rejectChannelInvite(id!, type, message: message); } - /// Add members to the channel + /// Add members to the channel. Future addMembers( List memberIds, [ Message? message, @@ -848,7 +855,7 @@ class Channel { return _client.addChannelMembers(id!, type, memberIds, message: message); } - /// Invite members to the channel + /// Invite members to the channel. Future inviteMembers( List memberIds, [ Message? message, @@ -857,7 +864,7 @@ class Channel { return _client.inviteChannelMembers(id!, type, memberIds, message: message); } - /// Remove members from the channel + /// Remove members from the channel. Future removeMembers( List memberIds, [ Message? message, @@ -866,7 +873,7 @@ class Channel { return _client.removeChannelMembers(id!, type, memberIds, message: message); } - /// Send action for a specific message of this channel + /// Send action for a specific message of this channel. Future sendAction( Message message, Map formData, @@ -913,9 +920,10 @@ class Channel { return res; } - /// Mark all messages as read + /// Mark all messages as read. + /// /// Optionally provide a [messageId] if you want to mark a - /// particular message as read + /// particular message as read. Future markRead({String? messageId}) async { _checkInitialized(); client.state.totalUnreadCount = @@ -924,7 +932,7 @@ class Channel { return _client.markChannelRead(id!, type, messageId: messageId); } - /// Loads the initial channel state and watches for changes + /// Loads the initial channel state and watches for changes. Future watch() async { ChannelState response; @@ -955,15 +963,16 @@ class Channel { } } - /// Stop watching the channel + /// Stop watching the channel. Future stopWatching() async { _checkInitialized(); return _client.stopChannelWatching(id!, type); } - /// List the message replies for a parent message + /// List the message replies for a parent message. + /// /// Set [preferOffline] to true to avoid the api call if the data is already - /// in the offline storage + /// in the offline storage. Future getReplies( String parentId, { PaginationParams? options, @@ -987,7 +996,7 @@ class Channel { return repliesResponse; } - /// List the reactions for a message in the channel + /// List the reactions for a message in the channel. Future getReactions( String messageId, { PaginationParams? pagination, @@ -997,7 +1006,7 @@ class Channel { pagination: pagination, ); - /// Retrieves a list of messages by ID + /// Retrieves a list of messages by given [messageIDs]. Future getMessagesById( List messageIDs, ) async { @@ -1008,7 +1017,7 @@ class Channel { return res; } - /// Retrieves a list of messages by ID + /// Translate a message by given [messageId] and [language]. Future translateMessage( String messageId, String language, @@ -1018,12 +1027,13 @@ class Channel { language, ); - /// Creates a new channel + /// Creates a new channel. Future create() async => query(state: false); - /// Query the API, get messages, members or other channel fields - /// Set [preferOffline] to true to avoid the api call if the data is already - /// in the offline storage + /// Query the API, get messages, members or other channel fields. + /// + /// Set [preferOffline] to true to avoid the API call if the data is already + /// in the offline storage. Future query({ bool state = true, bool watch = false, @@ -1077,7 +1087,7 @@ class Channel { } } - /// Query channel members + /// Query channel members. Future queryMembers({ Filter? filter, List? sort, @@ -1092,19 +1102,19 @@ class Channel { pagination: pagination, ); - /// Mutes the channel + /// Mutes the channel. Future mute({Duration? expiration}) { _checkInitialized(); return _client.muteChannel(cid!, expiration: expiration); } - /// Unmutes the channel + /// Unmute the channel. Future unmute() { _checkInitialized(); return _client.unmuteChannel(cid!); } - /// Bans a user from the channel + /// Bans the user with given [userID] from the channel. Future banUser( String userID, Map options, @@ -1118,7 +1128,7 @@ class Channel { return _client.banUser(userID, opts); } - /// Remove the ban for a user in the channel + /// Remove the ban for the user with given [userID] in the channel. Future unbanUser(String userID) async { _checkInitialized(); return _client.unbanUser(userID, { @@ -1127,7 +1137,7 @@ class Channel { }); } - /// Shadow bans a user from the channel + /// Shadow bans the user with the given [userID] from the channel. Future shadowBan( String userID, Map options, @@ -1141,7 +1151,7 @@ class Channel { return _client.shadowBan(userID, opts); } - /// Remove the shadow ban for a user in the channel + /// Remove the shadow ban for the user with the given [userID] in the channel. Future removeShadowBan(String userID) async { _checkInitialized(); return _client.removeShadowBan(userID, { @@ -1151,8 +1161,10 @@ class Channel { } /// Hides the channel from [StreamChatClient.queryChannels] for the user - /// until a message is added If [clearHistory] is set to true - all messages - /// will be removed for the user + /// until a message is added. + /// + /// If [clearHistory] is set to true - all messages + /// will be removed for the user. Future hide({bool clearHistory = false}) async { _checkInitialized(); final response = await _client.hideChannel( @@ -1170,7 +1182,7 @@ class Channel { return response; } - /// Removes the hidden status for the channel + /// Removes the hidden status for the channel. Future show() async { _checkInitialized(); return _client.showChannel(id!, type); @@ -1178,7 +1190,7 @@ class Channel { /// Stream of [Event] coming from websocket connection specific for the /// channel. Pass an eventType as parameter in order to filter just a type - /// of event + /// of event. Stream on([ String? eventType, String? eventType2, @@ -1216,7 +1228,7 @@ class Channel { } } - /// Sets last typing to null and sends the typing.stop event + /// Sets last typing to null and sends the typing.stop event. Future stopTyping([String? parentId]) async { if (config?.typingEvents == false) { return; @@ -1230,7 +1242,7 @@ class Channel { )); } - /// Call this method to dispose the channel client + /// Call this method to dispose the channel client. void dispose() { state?.dispose(); } @@ -1244,9 +1256,9 @@ class Channel { } } -/// The class that handles the state of the channel listening to the events +/// The class that handles the state of the channel listening to the events. class ChannelClientState { - /// Creates a new instance listening to events and updating the state + /// Creates a new instance listening to events and updating the state. ChannelClientState( this._channel, ChannelState channelState, @@ -1393,23 +1405,25 @@ class ChannelClientState { } /// Flag which indicates if [ChannelClientState] contain latest/recent messages or not. + /// /// This flag should be managed by UI sdks. - /// When false, any new message (received by WebSocket event - /// - [EventType.messageNew]) will not be pushed on to message list. + /// + /// When false, any new message received by WebSocket event + /// [EventType.messageNew] will not be pushed on to message list. bool get isUpToDate => _isUpToDateController.value; set isUpToDate(bool isUpToDate) => _isUpToDateController.add(isUpToDate); - /// [isUpToDate] flag count as a stream + /// [isUpToDate] flag count as a stream. Stream get isUpToDateStream => _isUpToDateController.stream; final BehaviorSubject _isUpToDateController = BehaviorSubject.seeded(true); - /// The retry queue associated to this channel + /// The retry queue associated to this channel. late final RetryQueue _retryQueue; - /// Retry failed message + /// Retry failed message. Future retryFailedMessages() async { final failedMessages = [...messages, ...threads.values.expand((v) => v)] @@ -1502,7 +1516,7 @@ class ChannelClientState { })); } - /// Add a message to this channel + /// Add a message to this channel. void addMessage(Message message) { if (message.parentId == null || message.showInChannel == true) { final newMessages = List.from(_channelState.messages); @@ -1567,36 +1581,36 @@ class ChannelClientState { ); } - /// Channel message list + /// Channel message list. List get messages => _channelState.messages; - /// Channel message list as a stream + /// Channel message list as a stream. Stream?> get messagesStream => channelStateStream .map((cs) => cs.messages) .distinct(const ListEquality().equals); - /// Channel pinned message list + /// Channel pinned message list. List? get pinnedMessages => _channelState.pinnedMessages.toList(); - /// Channel pinned message list as a stream + /// Channel pinned message list as a stream. Stream?> get pinnedMessagesStream => channelStateStream.map((cs) => cs.pinnedMessages.toList()); - /// Get channel last message + /// Get channel last message. Message? get lastMessage => _channelState.messages.isNotEmpty == true ? _channelState.messages.last : null; - /// Get channel last message + /// Get channel last message. Stream get lastMessageStream => messagesStream .map((event) => event?.isNotEmpty == true ? event!.last : null); - /// Channel members list + /// Channel members list. List get members => _channelState.members .map((e) => e.copyWith(user: _channel.client.state.users[e.user!.id])) .toList(); - /// Channel members list as a stream + /// Channel members list as a stream. Stream> get membersStream => CombineLatestStream.combine2< List?, Map, List>( channelStateStream.map((cs) => cs.members), @@ -1605,19 +1619,19 @@ class ChannelClientState { members!.map((e) => e!.copyWith(user: users[e.user!.id])).toList(), ).distinct(const ListEquality().equals); - /// Channel watcher count + /// Channel watcher count. int? get watcherCount => _channelState.watcherCount; - /// Channel watcher count as a stream + /// Channel watcher count as a stream. Stream get watcherCountStream => channelStateStream.map((cs) => cs.watcherCount); - /// Channel watchers list + /// Channel watchers list. List get watchers => _channelState.watchers .map((e) => _channel.client.state.users[e.id] ?? e) .toList(); - /// Channel watchers list as a stream + /// Channel watchers list as a stream. Stream> get watchersStream => CombineLatestStream.combine2< List?, Map, List>( channelStateStream.map((cs) => cs.watchers), @@ -1625,20 +1639,20 @@ class ChannelClientState { (watchers, users) => watchers!.map((e) => users[e.id] ?? e).toList(), ); - /// Channel read list + /// Channel read list. List? get read => _channelState.read; - /// Channel read list as a stream + /// Channel read list as a stream. Stream?> get readStream => channelStateStream.map((cs) => cs.read); final BehaviorSubject _unreadCountController = BehaviorSubject.seeded(0); set unreadCount(int value) => _unreadCountController.add(value); - /// Unread count getter as a stream + /// Unread count getter as a stream. Stream get unreadCountStream => _unreadCountController.stream.distinct(); - /// Unread count getter + /// Unread count getter. int get unreadCount => _unreadCountController.value; bool _countMessageAsUnread(Message message) { @@ -1654,7 +1668,7 @@ class ChannelClientState { !userIsMuted; } - /// Update threads with updated information about messages + /// Update threads with updated information about messages. void updateThreadInfo(String parentId, List messages) { final newThreads = Map>.from(threads); @@ -1676,7 +1690,7 @@ class ChannelClientState { _threads = newThreads; } - /// Delete all channel messages + /// Delete all channel messages. void truncate() { _channelState = _channelState.copyWith( messages: [], @@ -1685,7 +1699,7 @@ class ChannelClientState { final List _updatedMessagesIds = []; - /// Update channelState with updated information + /// Update channelState with updated information. void updateChannelState(ChannelState updatedState) { final newMessages = [ ...updatedState.messages, @@ -1737,13 +1751,13 @@ class ChannelClientState { int _sortByCreatedAt(Message a, Message b) => a.createdAt.compareTo(b.createdAt); - /// The channel state related to this client + /// The channel state related to this client. ChannelState get _channelState => _channelStateController.value; - /// The channel state related to this client as a stream + /// The channel state related to this client as a stream. Stream get channelStateStream => _channelStateController.stream; - /// The channel state related to this client + /// The channel state related to this client. ChannelState get channelState => _channelStateController.value; late BehaviorSubject _channelStateController; @@ -1754,11 +1768,11 @@ class ChannelClientState { _debouncedUpdatePersistenceChannelState.call([v]); } - /// The channel threads related to this channel + /// The channel threads related to this channel. Map> get threads => _threadsController.value.map((key, value) => MapEntry(key, value)); - /// The channel threads related to this channel as a stream + /// The channel threads related to this channel as a stream. Stream>> get threadsStream => _threadsController.stream; final BehaviorSubject>> _threadsController = @@ -1772,10 +1786,10 @@ class ChannelClientState { _threadsController.add(v); } - /// Channel related typing users last value + /// Channel related typing users last value. Map get typingEvents => _typingEventsController.value; - /// Channel related typing users stream + /// Channel related typing users stream. Stream> get typingEventsStream => _typingEventsController.stream; @@ -1903,7 +1917,7 @@ class ChannelClientState { }); } - /// Call this method to dispose this object + /// Call this method to dispose this object. void dispose() { _debouncedUpdatePersistenceChannelState.cancel(); _unreadCountController.close(); From eae7cb10206264bfd1570dcf96f3d998910d41f6 Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Tue, 10 Aug 2021 09:59:08 +0200 Subject: [PATCH 085/121] feat: add image get, set and update to channel --- .../stream_chat/lib/src/client/channel.dart | 110 ++++++++++++++++-- .../test/src/api/channel_test.dart | 54 +++++++++ 2 files changed, 155 insertions(+), 9 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 4d4a3858..7ea1a50b 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -15,20 +15,49 @@ import 'package:stream_chat/src/core/util/utils.dart'; import 'package:stream_chat/src/event_type.dart'; import 'package:stream_chat/stream_chat.dart'; -/// This a the class that manages a specific channel. +/// Class that manages a specific channel. +/// +/// {@template image} +/// If an optional [image] argument is provided in the constructor then it +/// will be set on [extraData] with a key of 'image'. +/// +/// ```dart +/// final channel = Channel(client, type, id, image: 'https://getstream.io/image.png'); +/// print(channel.image == channel.extraData['image']); // true +/// ``` +/// +/// Before the channel is initialized the image can be set directly: +/// ```dart +/// channel.image = 'https://getstream.io/new-image'; +/// ``` +/// +/// To update the image after the channel has been initialized call: +/// ```dart +/// channel.updateImage('https://getstream.io/new-image'); +/// ``` +/// +/// This will do a partial update to update the image. +/// {@endtemplate} class Channel { - /// Create a channel client instance. + /// Class that manages a specific channel. + /// + /// Optional [extraData] and [image] properties can be provided. The [image] + /// is exposed to easily set a key of 'image' on [extraData]. Channel( this._client, this._type, this._id, { Map? extraData, + String? image, }) : _cid = _id != null ? '$_type:$_id' : null, - _extraData = extraData ?? {} { - _client.logger.info('New Channel instance not initialized created'); + _extraData = { + ...?extraData, + if (image != null) 'image': image, + } { + _client.logger.info('New Channel instance created, not yet initialized'); } - /// Create a channel client instance from a [ChannelState] object + /// Create a channel client instance from a [ChannelState] object. Channel.fromState(this._client, ChannelState channelState) : assert( channelState.channel != null, @@ -40,7 +69,7 @@ class Channel { _extraData = channelState.channel!.extraData { state = ChannelClientState(this, channelState); _initializedCompleter.complete(true); - _client.logger.info('New Channel instance initialized created'); + _client.logger.info('New Channel instance initialized'); } /// This client state @@ -63,6 +92,19 @@ class Channel { _extraData.addAll(extraData); } + /// Shortcut to set channel image. + /// + /// {@macro image} + set image(String? image) { + if (_initializedCompleter.isCompleted) { + throw StateError( + 'Once the channel is initialized you should use channel.update ' + 'to update channel image', + ); + } + _extraData.addAll({'image': image}); + } + /// Returns true if the channel is muted. bool get isMuted => _client.state.currentUser?.channelMutes @@ -218,7 +260,26 @@ class Channel { ); } - /// The main Stream chat client + /// Shortcut to get channel image. + /// + /// {@macro image} + String? get image => extraData['image'] as String?; + + /// Channel [image] as a stream. + /// + /// The channel needs to be initialized. + /// + /// {@macro image} + Stream get imageStream { + _checkInitialized(); + return state!.channelStateStream.map( + (cs) => + (cs.channel?.extraData['image'] as String?) ?? + (_extraData['image'] as String?), + ); + } + + /// The main Stream chat client. StreamChatClient get client => _client; final StreamChatClient _client; @@ -799,7 +860,36 @@ class Channel { } } - /// Edit the channel custom data + /// Update the channel's [image]. + /// + /// This is equivelant to calling [updatePartial] and providing a map with an + /// 'image' key: + /// + /// ```dart + /// channel.updatePartial( + /// set: {'image': 'https://getstream.io/new-image'} + /// ); + /// ``` + /// + /// Instead do: + /// ```dart + /// channel.updateImage('https://getstream.io/new-image'); + /// ``` + Future updateImage( + String image, + ) { + _checkInitialized(); + + return _client.updateChannelPartial( + id!, + type, + set: { + 'image': image, + }, + ); + } + + /// Edit the channel custom data. Future update( Map channelData, [ Message? updateMessage, @@ -813,7 +903,9 @@ class Channel { ); } - /// Edit the channel custom data + /// Edit the channel custom data. + // TODO: This is the same description as [update]. Distinguish the two + // and provide a better description for set and unset. Future updatePartial({ Map? set, List? unset, diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/api/channel_test.dart index 5be55491..381c208b 100644 --- a/packages/stream_chat/test/src/api/channel_test.dart +++ b/packages/stream_chat/test/src/api/channel_test.dart @@ -72,6 +72,23 @@ void main() { expect(channel.extraData.containsKey('name'), isTrue); expect(channel.extraData['name'], 'test-channel-name'); }); + + test('should be able to get and set `image`', () { + expect(channel.extraData.isEmpty, isTrue); + + const imageUrl = 'https://getstream.io/some-image'; + channel.image = imageUrl; + + expect(channel.image, imageUrl); + expect(channel.extraData['image'], imageUrl); + + const newImage = 'https://getstream.io/new-image'; + final newChannelInstance = + Channel(client, channelType, channelId, image: newImage); + + expect(newChannelInstance.image, newImage); + expect(newChannelInstance.extraData['image'], newImage); + }); }); // TODO : test all persistence related logic in this group @@ -192,6 +209,14 @@ void main() { } }); + test('should throw if trying to set `image`', () { + try { + channel.image = 'https://stream.io/some-image'; + } catch (e) { + expect(e, isA()); + } + }); + group('`.sendMessage`', () { test('should work fine', () async { final message = Message(id: 'test-message-id'); @@ -1192,6 +1217,35 @@ void main() { message: any(named: 'message'))).called(1); }); + test('`.updateImage`', () async { + const image = 'https://getstream.io/new-image'; + + final channelModel = ChannelModel( + cid: channelCid, + extraData: {'image': image}, + ); + + when(() => client.updateChannelPartial( + any(), + any(), + set: {'image': image}, + )).thenAnswer( + (_) async => PartialUpdateChannelResponse()..channel = channelModel, + ); + final res = await channel.updateImage(image); + + expect(res, isNotNull); + expect(res.channel.extraData['image'], image); + + verify( + () => client.updateChannelPartial( + any(), + any(), + set: {'image': image}, + ), + ).called(1); + }); + test('`.updatePartial`', () async { const set = { 'name': 'Stream Team', From ae8ebe2dba6e6a1c3a175bb506c18610bbf7c7ab Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Tue, 10 Aug 2021 10:00:02 +0200 Subject: [PATCH 086/121] chore: prefer image and name property on image --- .../test/src/core/models/reaction_test.dart | 28 ++++++++++--------- .../test/src/channel_image_test.dart | 26 +++++------------ 2 files changed, 22 insertions(+), 32 deletions(-) diff --git a/packages/stream_chat/test/src/core/models/reaction_test.dart b/packages/stream_chat/test/src/core/models/reaction_test.dart index 0891b548..fbe493ef 100644 --- a/packages/stream_chat/test/src/core/models/reaction_test.dart +++ b/packages/stream_chat/test/src/core/models/reaction_test.dart @@ -13,10 +13,11 @@ void main() { expect(reaction.type, 'wow'); expect( reaction.user?.toJson(), - User(id: '2de0297c-f3f2-489d-b930-ef77342edccf', extraData: const { - 'image': 'https://randomuser.me/api/portraits/women/45.jpg', - 'name': 'Daisy Morgan' - }).toJson(), + User( + id: '2de0297c-f3f2-489d-b930-ef77342edccf', + image: 'https://randomuser.me/api/portraits/women/45.jpg', + name: 'Daisy Morgan', + ).toJson(), ); expect(reaction.score, 1); expect(reaction.userId, '2de0297c-f3f2-489d-b930-ef77342edccf'); @@ -28,11 +29,11 @@ void main() { messageId: '76cd8c82-b557-4e48-9d12-87995d3a0e04', createdAt: DateTime.parse('2020-01-28T22:17:31.108742Z'), type: 'wow', - user: - User(id: '2de0297c-f3f2-489d-b930-ef77342edccf', extraData: const { - 'image': 'https://randomuser.me/api/portraits/women/45.jpg', - 'name': 'Daisy Morgan' - }), + user: User( + id: '2de0297c-f3f2-489d-b930-ef77342edccf', + image: 'https://randomuser.me/api/portraits/women/45.jpg', + name: 'Daisy Morgan', + ), userId: '2de0297c-f3f2-489d-b930-ef77342edccf', extraData: {'bananas': 'yes'}, score: 1, @@ -58,10 +59,11 @@ void main() { expect(newReaction.type, 'wow'); expect( newReaction.user?.toJson(), - User(id: '2de0297c-f3f2-489d-b930-ef77342edccf', extraData: const { - 'image': 'https://randomuser.me/api/portraits/women/45.jpg', - 'name': 'Daisy Morgan', - }).toJson(), + User( + id: '2de0297c-f3f2-489d-b930-ef77342edccf', + image: 'https://randomuser.me/api/portraits/women/45.jpg', + name: 'Daisy Morgan', + ).toJson(), ); expect(newReaction.score, 1); expect(newReaction.userId, '2de0297c-f3f2-489d-b930-ef77342edccf'); diff --git a/packages/stream_chat_flutter/test/src/channel_image_test.dart b/packages/stream_chat_flutter/test/src/channel_image_test.dart index c9293e79..f6d110dd 100644 --- a/packages/stream_chat_flutter/test/src/channel_image_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_image_test.dart @@ -48,7 +48,7 @@ void main() { ); testWidgets( - 'it should show the the other member image', + 'it should show the other member image', (tester) async { final client = MockClient(); final clientState = MockClientState(); @@ -74,9 +74,7 @@ void main() { userId: 'user-id2', user: User( id: 'user-id2', - extraData: const { - 'image': 'testimage', - }, + image: 'testimage', ), ) ])); @@ -85,9 +83,7 @@ void main() { userId: 'user-id2', user: User( id: 'user-id2', - extraData: const { - 'image': 'testimage', - }, + image: 'testimage', ), ), Member( @@ -98,9 +94,7 @@ void main() { when(() => clientState.usersStream).thenAnswer((i) => Stream.value({ 'user-id2': User( id: 'user-id2', - extraData: const { - 'image': 'testimage', - }, + image: 'testimage', ), })); when(() => channel.extraData).thenReturn({ @@ -149,27 +143,21 @@ void main() { userId: 'user-id', user: User( id: 'user-id', - extraData: const { - 'image': 'testimage1', - }, + image: 'testimage1', ), ), Member( userId: 'user-id2', user: User( id: 'user-id2', - extraData: const { - 'image': 'testimage2', - }, + image: 'testimage2', ), ), Member( userId: 'user-id3', user: User( id: 'user-id3', - extraData: const { - 'image': 'testimage3', - }, + image: 'testimage3', ), ), ]; From 131019d77af5c5186a2005b1fbe13cbb92c6df0a Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Tue, 10 Aug 2021 10:09:37 +0200 Subject: [PATCH 087/121] chore: update CHANGELOG.md --- packages/stream_chat/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index e3e31db6..513c1cdb 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -8,7 +8,7 @@ - `User` and `OwnUser` classes now have an `image` property. Setting an image will also set the 'image' key on `extraData`, so `user.image` and `user.extraData['image']` is the same. - `User` and `OwnUser` classes now have a `name` property. Setting a name will also set the 'name' key on `extraData`, so `user.name` and `user.extraData['name']` is the same. - +- `Channel` class now has extra `image` getter and setter. As well as an `updateImage` to do a partial update after a channel has been initialized. ## 2.1.1 🐞 Fixed From adfc0adea97362909f7ed91f3b1652cc5812f4ab Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Tue, 10 Aug 2021 11:53:30 +0200 Subject: [PATCH 088/121] feat: add name get, set and update on Channel --- .../stream_chat/lib/src/client/channel.dart | 113 +++++++++++++++--- .../test/src/api/channel_test.dart | 59 +++++++++ .../example/lib/main.dart | 16 +-- 3 files changed, 160 insertions(+), 28 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 7ea1a50b..aeee3540 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -17,6 +17,32 @@ import 'package:stream_chat/stream_chat.dart'; /// Class that manages a specific channel. /// +/// #### Channel name +/// +/// {@template name} +/// If an optional [name] argument is provided in the constructor then it +/// will be set on [extraData] with a key of 'name'. +/// +/// ```dart +/// final channel = Channel(client, type, id, name: 'Channel name'); +/// print(channel.name == channel.extraData['name']); // true +/// ``` +/// +/// Before the channel is initialized the name can be set directly: +/// ```dart +/// channel.name = 'New channel name'; +/// ``` +/// +/// To update the name after the channel has been initialized, call: +/// ```dart +/// channel.updateName('Updated channel name'); +/// ``` +/// +/// This will do a partial update to update the name. +/// {@endtemplate} +/// +/// #### Channel image +/// /// {@template image} /// If an optional [image] argument is provided in the constructor then it /// will be set on [extraData] with a key of 'image'. @@ -31,7 +57,7 @@ import 'package:stream_chat/stream_chat.dart'; /// channel.image = 'https://getstream.io/new-image'; /// ``` /// -/// To update the image after the channel has been initialized call: +/// To update the image after the channel has been initialized, call: /// ```dart /// channel.updateImage('https://getstream.io/new-image'); /// ``` @@ -49,10 +75,12 @@ class Channel { this._id, { Map? extraData, String? image, + String? name, }) : _cid = _id != null ? '$_type:$_id' : null, _extraData = { ...?extraData, if (image != null) 'image': image, + if (name != null) 'name': name, } { _client.logger.info('New Channel instance created, not yet initialized'); } @@ -98,13 +126,26 @@ class Channel { set image(String? image) { if (_initializedCompleter.isCompleted) { throw StateError( - 'Once the channel is initialized you should use channel.update ' - 'to update channel image', + 'Once the channel is initialized you should use channel.updateImage ' + 'to update the channel image', ); } _extraData.addAll({'image': image}); } + /// Shortcut to set channel name. + /// + /// {@macro name} + set name(String? name) { + if (_initializedCompleter.isCompleted) { + throw StateError( + 'Once the channel is initialized you should use channel.updateName ' + 'to update the channel image', + ); + } + _extraData.addAll({'name': name}); + } + /// Returns true if the channel is muted. bool get isMuted => _client.state.currentUser?.channelMutes @@ -279,6 +320,39 @@ class Channel { ); } + /// Shortcut to get channel name. + /// + /// If no name is set this returns the channel cid, else null. + /// + /// {@macro name} + String? get name { + if (extraData.containsKey('name')) { + final name = extraData['name']! as String; + if (name.isNotEmpty) return name; + } + return cid; + } + + /// Channel [name] as a stream. + /// + /// If no name is set the stream returns the channel cid. + /// + /// The channel needs to be initialized. + /// + /// {@macro name} + Stream get nameStream { + _checkInitialized(); + return state!.channelStateStream.map( + (cs) { + if (cs.channel?.extraData.containsKey('name') ?? false) { + final name = cs.channel!.extraData['name']! as String; + if (name.isNotEmpty) return name; + } + return name!; + }, + ); + } + /// The main Stream chat client. StreamChatClient get client => _client; final StreamChatClient _client; @@ -862,7 +936,7 @@ class Channel { /// Update the channel's [image]. /// - /// This is equivelant to calling [updatePartial] and providing a map with an + /// This is the same as calling [updatePartial] and providing a map with an /// 'image' key: /// /// ```dart @@ -877,17 +951,28 @@ class Channel { /// ``` Future updateImage( String image, - ) { - _checkInitialized(); + ) => + updatePartial(set: {'image': image}); - return _client.updateChannelPartial( - id!, - type, - set: { - 'image': image, - }, - ); - } + /// Update the channel's [name]. + /// + /// This is the same as calling [updatePartial] and providing a map with a + /// 'name' key: + /// + /// ```dart + /// channel.updatePartial( + /// set: {'name': 'Updated channel name'} + /// ); + /// ``` + /// + /// Instead do: + /// ```dart + /// channel.updateName('Updated channel name'); + /// ``` + Future updateName( + String name, + ) => + updatePartial(set: {'name': name}); /// Edit the channel custom data. Future update( diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/api/channel_test.dart index 381c208b..1166405a 100644 --- a/packages/stream_chat/test/src/api/channel_test.dart +++ b/packages/stream_chat/test/src/api/channel_test.dart @@ -89,6 +89,28 @@ void main() { expect(newChannelInstance.image, newImage); expect(newChannelInstance.extraData['image'], newImage); }); + + test('should be able to get and set `name`', () { + expect(channel.extraData.isEmpty, isTrue); + expect( + channel.name, + channelId, + reason: 'if name is not set then use channel id', + ); + + const name = 'Channel name'; + channel.name = name; + + expect(channel.name, name); + expect(channel.extraData['name'], name); + + const newName = 'New channel name'; + final newChannelInstance = + Channel(client, channelType, channelId, name: newName); + + expect(newChannelInstance.name, newName); + expect(newChannelInstance.extraData['name'], newName); + }); }); // TODO : test all persistence related logic in this group @@ -217,6 +239,14 @@ void main() { } }); + test('should throw if trying to set `name`', () { + try { + channel.name = 'New name'; + } catch (e) { + expect(e, isA()); + } + }); + group('`.sendMessage`', () { test('should work fine', () async { final message = Message(id: 'test-message-id'); @@ -1246,6 +1276,35 @@ void main() { ).called(1); }); + test('`.updateName`', () async { + const name = 'Name'; + + final channelModel = ChannelModel( + cid: channelCid, + extraData: {'name': name}, + ); + + when(() => client.updateChannelPartial( + any(), + any(), + set: {'name': name}, + )).thenAnswer( + (_) async => PartialUpdateChannelResponse()..channel = channelModel, + ); + final res = await channel.updateName(name); + + expect(res, isNotNull); + expect(res.channel.extraData['name'], name); + + verify( + () => client.updateChannelPartial( + any(), + any(), + set: {'name': name}, + ), + ).called(1); + }); + test('`.updatePartial`', () async { const set = { 'name': 'Stream Team', diff --git a/packages/stream_chat_flutter_core/example/lib/main.dart b/packages/stream_chat_flutter_core/example/lib/main.dart index 03e791de..31410e63 100644 --- a/packages/stream_chat_flutter_core/example/lib/main.dart +++ b/packages/stream_chat_flutter_core/example/lib/main.dart @@ -329,21 +329,9 @@ class _MessageScreenState extends State { } } -/// Extensions can be used to add functionality to the SDK. In the examples -/// below, we add two simple extensions to the [StreamChatClient] and [Channel]. +/// Extensions can be used to add functionality to the SDK. In the example +/// below, we add a simple extensions to the [StreamChatClient]. extension on StreamChatClient { /// Fetches the current user id. String get uid => state.currentUser!.id; } - -extension on Channel { - /// Fetches the name of the channel by accessing [extraData] or [cid]. - String? get name { - final _channelName = extraData['name']; - if (_channelName != null) { - return _channelName as String; - } else { - return cid; - } - } -} From 472b50397f4ae19266a50647217aa99fd56e69f9 Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Tue, 10 Aug 2021 11:54:31 +0200 Subject: [PATCH 089/121] chore: update CHANGELOG.md --- packages/stream_chat/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 513c1cdb..52496a96 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -9,6 +9,7 @@ - `User` and `OwnUser` classes now have an `image` property. Setting an image will also set the 'image' key on `extraData`, so `user.image` and `user.extraData['image']` is the same. - `User` and `OwnUser` classes now have a `name` property. Setting a name will also set the 'name' key on `extraData`, so `user.name` and `user.extraData['name']` is the same. - `Channel` class now has extra `image` getter and setter. As well as an `updateImage` to do a partial update after a channel has been initialized. +- `Channel` class now has extra `name` getter and setter. As well as an `updateName` to do a partial update after a channel has been initialized. ## 2.1.1 🐞 Fixed From 250162b7bdf3de0c4ad6364d36f97cd7019eea9d Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Tue, 10 Aug 2021 11:56:32 +0200 Subject: [PATCH 090/121] use image getter --- packages/stream_chat/lib/src/client/channel.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index aeee3540..5181eaad 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -314,9 +314,7 @@ class Channel { Stream get imageStream { _checkInitialized(); return state!.channelStateStream.map( - (cs) => - (cs.channel?.extraData['image'] as String?) ?? - (_extraData['image'] as String?), + (cs) => (cs.channel?.extraData['image'] as String?) ?? image, ); } From 4dff4d4cbe66fed7140344f6b0fdb1ff26f229af Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Tue, 10 Aug 2021 12:17:19 +0200 Subject: [PATCH 091/121] chore: additional descriptions for channel update --- .../stream_chat/lib/src/client/channel.dart | 23 +++++++++++++++---- .../stream_chat/lib/src/client/client.dart | 9 ++++++-- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 5181eaad..7bf91caf 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -972,7 +972,13 @@ class Channel { ) => updatePartial(set: {'name': name}); - /// Edit the channel custom data. + /// Update the channel custom data. This replaces all of the channel data + /// with the given [channelData]. + /// + /// If you instead want to do a partial update, use [updatePartial]. + /// + /// See, https://getstream.io/chat/docs/other-rest/channel_update/?language=dart + /// for more information. Future update( Map channelData, [ Message? updateMessage, @@ -986,9 +992,18 @@ class Channel { ); } - /// Edit the channel custom data. - // TODO: This is the same description as [update]. Distinguish the two - // and provide a better description for set and unset. + /// A partial update can be used to set and unset specific custom data fields + /// when it is necessary to retain additional custom data fields on the + /// object. + /// + /// - [set] will add, or update existing attributes. + /// - [unset] will remove the attributes with the provided list of + /// values (keys). + /// + /// If you want to do a full update/replacement, use [update] instead. + /// + /// See, https://getstream.io/chat/docs/other-rest/channel_update/?language=dart + /// for more information. Future updatePartial({ Map? set, List? unset, diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 104b23fc..74abcb39 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -767,7 +767,9 @@ class StreamChatClient { cancelToken: cancelToken, ); - /// Replaces the [channelId] of type [ChannelType] data with [data] + /// Replaces the [channelId] of type [ChannelType] data with [data]. + /// + /// Use [updateChannelPartial] for a partial update. Future updateChannel( String channelId, String channelType, @@ -781,7 +783,10 @@ class StreamChatClient { message: message, ); - /// Updates the [channelId] of type [ChannelType] data with [data] + /// Partial update for the [channelId] of type [ChannelType]. Sets the + /// data provided in [set], and removes the attributes given in [unset]. + /// + /// Use [updateChannel] for a full update. Future updateChannelPartial( String channelId, String channelType, { From 5e2c6975e21642f871b9febd519882d4824a7cb7 Mon Sep 17 00:00:00 2001 From: Gordon Hayes Date: Tue, 10 Aug 2021 13:59:41 +0200 Subject: [PATCH 092/121] chore: update error description --- packages/stream_chat/lib/src/client/channel.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 7bf91caf..656d7f38 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -140,7 +140,7 @@ class Channel { if (_initializedCompleter.isCompleted) { throw StateError( 'Once the channel is initialized you should use channel.updateName ' - 'to update the channel image', + 'to update the channel name', ); } _extraData.addAll({'name': name}); From b3a1d42f92d49a15f8746859269a2ca1e4096798 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 11 Aug 2021 12:47:20 +0530 Subject: [PATCH 093/121] chore(llc): fix tests, minor changes Signed-off-by: xsahil03x --- .../stream_chat/lib/src/client/channel.dart | 112 +++++++++--------- .../test/src/api/channel_test.dart | 37 +++--- 2 files changed, 73 insertions(+), 76 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 656d7f38..5eee8060 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -73,14 +73,14 @@ class Channel { this._client, this._type, this._id, { - Map? extraData, - String? image, String? name, + String? image, + Map? extraData, }) : _cid = _id != null ? '$_type:$_id' : null, _extraData = { ...?extraData, - if (image != null) 'image': image, if (name != null) 'name': name, + if (image != null) 'image': image, } { _client.logger.info('New Channel instance created, not yet initialized'); } @@ -110,14 +110,17 @@ class Channel { String? _cid; final Map _extraData; - set extraData(Map extraData) { + /// Shortcut to set channel name. + /// + /// {@macro name} + set name(String? name) { if (_initializedCompleter.isCompleted) { throw StateError( - 'Once the channel is initialized you should use channel.update ' - 'to update channel data', + 'Once the channel is initialized you should use `channel.updateName` ' + 'to update the channel name', ); } - _extraData.addAll(extraData); + _extraData.addAll({'name': name}); } /// Shortcut to set channel image. @@ -126,24 +129,21 @@ class Channel { set image(String? image) { if (_initializedCompleter.isCompleted) { throw StateError( - 'Once the channel is initialized you should use channel.updateImage ' + 'Once the channel is initialized you should use `channel.updateImage` ' 'to update the channel image', ); } _extraData.addAll({'image': image}); } - /// Shortcut to set channel name. - /// - /// {@macro name} - set name(String? name) { + set extraData(Map extraData) { if (_initializedCompleter.isCompleted) { throw StateError( - 'Once the channel is initialized you should use channel.updateName ' - 'to update the channel name', + 'Once the channel is initialized you should use `channel.update` ' + 'to update channel data', ); } - _extraData.addAll({'name': name}); + _extraData.addAll(extraData); } /// Returns true if the channel is muted. @@ -301,23 +301,6 @@ class Channel { ); } - /// Shortcut to get channel image. - /// - /// {@macro image} - String? get image => extraData['image'] as String?; - - /// Channel [image] as a stream. - /// - /// The channel needs to be initialized. - /// - /// {@macro image} - Stream get imageStream { - _checkInitialized(); - return state!.channelStateStream.map( - (cs) => (cs.channel?.extraData['image'] as String?) ?? image, - ); - } - /// Shortcut to get channel name. /// /// If no name is set this returns the channel cid, else null. @@ -342,15 +325,34 @@ class Channel { _checkInitialized(); return state!.channelStateStream.map( (cs) { - if (cs.channel?.extraData.containsKey('name') ?? false) { - final name = cs.channel!.extraData['name']! as String; + final extraData = cs.channel?.extraData; + if (extraData != null && extraData.containsKey('name')) { + final name = extraData['name']! as String; if (name.isNotEmpty) return name; } + // this can never be null once the channel is initialized return name!; }, ); } + /// Shortcut to get channel image. + /// + /// {@macro image} + String? get image => extraData['image'] as String?; + + /// Channel [image] as a stream. + /// + /// The channel needs to be initialized. + /// + /// {@macro image} + Stream get imageStream { + _checkInitialized(); + return state!.channelStateStream.map( + (cs) => (cs.channel?.extraData['image'] as String?) ?? image, + ); + } + /// The main Stream chat client. StreamChatClient get client => _client; final StreamChatClient _client; @@ -932,26 +934,6 @@ class Channel { } } - /// Update the channel's [image]. - /// - /// This is the same as calling [updatePartial] and providing a map with an - /// 'image' key: - /// - /// ```dart - /// channel.updatePartial( - /// set: {'image': 'https://getstream.io/new-image'} - /// ); - /// ``` - /// - /// Instead do: - /// ```dart - /// channel.updateImage('https://getstream.io/new-image'); - /// ``` - Future updateImage( - String image, - ) => - updatePartial(set: {'image': image}); - /// Update the channel's [name]. /// /// This is the same as calling [updatePartial] and providing a map with a @@ -967,11 +949,27 @@ class Channel { /// ```dart /// channel.updateName('Updated channel name'); /// ``` - Future updateName( - String name, - ) => + Future updateName(String name) => updatePartial(set: {'name': name}); + /// Update the channel's [image]. + /// + /// This is the same as calling [updatePartial] and providing a map with an + /// 'image' key: + /// + /// ```dart + /// channel.updatePartial( + /// set: {'image': 'https://getstream.io/new-image'} + /// ); + /// ``` + /// + /// Instead do: + /// ```dart + /// channel.updateImage('https://getstream.io/new-image'); + /// ``` + Future updateImage(String image) => + updatePartial(set: {'image': image}); + /// Update the channel custom data. This replaces all of the channel data /// with the given [channelData]. /// diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/api/channel_test.dart index 1166405a..6b3dfda2 100644 --- a/packages/stream_chat/test/src/api/channel_test.dart +++ b/packages/stream_chat/test/src/api/channel_test.dart @@ -39,6 +39,7 @@ void main() { late final client = MockStreamChatClient(); const channelId = 'test-channel-id'; const channelType = 'test-channel-type'; + const channelCid = '$channelType:$channelId'; late Channel channel; setUpAll(() { @@ -94,7 +95,7 @@ void main() { expect(channel.extraData.isEmpty, isTrue); expect( channel.name, - channelId, + channelCid, reason: 'if name is not set then use channel id', ); @@ -1256,24 +1257,23 @@ void main() { ); when(() => client.updateChannelPartial( - any(), - any(), + channelId, + channelType, set: {'image': image}, )).thenAnswer( (_) async => PartialUpdateChannelResponse()..channel = channelModel, ); + final res = await channel.updateImage(image); expect(res, isNotNull); expect(res.channel.extraData['image'], image); - verify( - () => client.updateChannelPartial( - any(), - any(), - set: {'image': image}, - ), - ).called(1); + verify(() => client.updateChannelPartial( + channelId, + channelType, + set: {'image': image}, + )).called(1); }); test('`.updateName`', () async { @@ -1285,24 +1285,23 @@ void main() { ); when(() => client.updateChannelPartial( - any(), - any(), + channelId, + channelType, set: {'name': name}, )).thenAnswer( (_) async => PartialUpdateChannelResponse()..channel = channelModel, ); + final res = await channel.updateName(name); expect(res, isNotNull); expect(res.channel.extraData['name'], name); - verify( - () => client.updateChannelPartial( - any(), - any(), - set: {'name': name}, - ), - ).called(1); + verify(() => client.updateChannelPartial( + channelId, + channelType, + set: {'name': name}, + )).called(1); }); test('`.updatePartial`', () async { From 702f7ee9379f20e6e9b5bb6069aac4ddec31b70d Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 11 Aug 2021 15:00:05 +0530 Subject: [PATCH 094/121] fix(llc): don't return `cid` in case `name` is null, minor improvements Signed-off-by: xsahil03x --- .../stream_chat/lib/src/client/channel.dart | 30 +++---------------- .../test/src/api/channel_test.dart | 6 ---- 2 files changed, 4 insertions(+), 32 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 5eee8060..b22a57cc 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -303,37 +303,17 @@ class Channel { /// Shortcut to get channel name. /// - /// If no name is set this returns the channel cid, else null. - /// /// {@macro name} - String? get name { - if (extraData.containsKey('name')) { - final name = extraData['name']! as String; - if (name.isNotEmpty) return name; - } - return cid; - } + String? get name => extraData['name'] as String?; /// Channel [name] as a stream. /// - /// If no name is set the stream returns the channel cid. - /// /// The channel needs to be initialized. /// /// {@macro name} - Stream get nameStream { + Stream get nameStream { _checkInitialized(); - return state!.channelStateStream.map( - (cs) { - final extraData = cs.channel?.extraData; - if (extraData != null && extraData.containsKey('name')) { - final name = extraData['name']! as String; - if (name.isNotEmpty) return name; - } - // this can never be null once the channel is initialized - return name!; - }, - ); + return extraDataStream.map((it) => it['name'] as String?); } /// Shortcut to get channel image. @@ -348,9 +328,7 @@ class Channel { /// {@macro image} Stream get imageStream { _checkInitialized(); - return state!.channelStateStream.map( - (cs) => (cs.channel?.extraData['image'] as String?) ?? image, - ); + return extraDataStream.map((it) => it['image'] as String?); } /// The main Stream chat client. diff --git a/packages/stream_chat/test/src/api/channel_test.dart b/packages/stream_chat/test/src/api/channel_test.dart index 6b3dfda2..7c5386c6 100644 --- a/packages/stream_chat/test/src/api/channel_test.dart +++ b/packages/stream_chat/test/src/api/channel_test.dart @@ -39,7 +39,6 @@ void main() { late final client = MockStreamChatClient(); const channelId = 'test-channel-id'; const channelType = 'test-channel-type'; - const channelCid = '$channelType:$channelId'; late Channel channel; setUpAll(() { @@ -93,11 +92,6 @@ void main() { test('should be able to get and set `name`', () { expect(channel.extraData.isEmpty, isTrue); - expect( - channel.name, - channelCid, - reason: 'if name is not set then use channel id', - ); const name = 'Channel name'; channel.name = name; From f8797086c5b353d2d2b197e9a67303e8ef68df82 Mon Sep 17 00:00:00 2001 From: Gordon Date: Wed, 11 Aug 2021 15:42:52 +0200 Subject: [PATCH 095/121] docs: fix broken link --- docusaurus/docs/Flutter/guides/adding_localization.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docusaurus/docs/Flutter/guides/adding_localization.mdx b/docusaurus/docs/Flutter/guides/adding_localization.mdx index 37f7299d..b031796f 100644 --- a/docusaurus/docs/Flutter/guides/adding_localization.mdx +++ b/docusaurus/docs/Flutter/guides/adding_localization.mdx @@ -14,7 +14,7 @@ We have a dedicated package for adding localization to our UI widgets. It's call ## What is Localization? -If you deploy your app to users who speak another language, you'll need to internationalize (localize) it. That means you need to write the app in a way that makes it possible to localize values like text and layouts for each language or locale that the app supports. For more information, see the [Flutter documentation](https://flutter.dev/docs/development/accessibility-and-localization/**internationalization**). +If you deploy your app to users who speak another language, you'll need to internationalize (localize) it. That means you need to write the app in a way that makes it possible to localize values like text and layouts for each language or locale that the app supports. For more information, see the [Flutter documentation](https://flutter.dev/docs/development/accessibility-and-localization/internationalization). What this package allows you to do is to provide localized strings for the Stream chat widgets. For example, depending on the application locale, the Stream Chat widgets will display the appropriate language. The locale will be set automatically, based on system preferences, or you could set it programmatically in your app. The package supports several different languages, with more to be added. The package allows you to override any supported language or add a new language that isn't supported. From a15ae16f987d8c80daa83575543e33a69ab83752 Mon Sep 17 00:00:00 2001 From: Gordon Date: Wed, 11 Aug 2021 15:49:46 +0200 Subject: [PATCH 096/121] docs: add i18n to title --- docusaurus/docs/Flutter/guides/adding_localization.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docusaurus/docs/Flutter/guides/adding_localization.mdx b/docusaurus/docs/Flutter/guides/adding_localization.mdx index b031796f..3cad5696 100644 --- a/docusaurus/docs/Flutter/guides/adding_localization.mdx +++ b/docusaurus/docs/Flutter/guides/adding_localization.mdx @@ -1,7 +1,7 @@ --- id: adding_localization sidebar_position: 2 -title: Adding Localization +title: Adding Localization (i18n) --- Adding Localization To UI Widgets From 977a6fdc7d9dea613760d8f62b25845442c7d8f6 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 12 Aug 2021 18:56:58 +0530 Subject: [PATCH 097/121] Update docusaurus/docs/Flutter/guides/understanding_filters.mdx Co-authored-by: Gordon Hayes --- docusaurus/docs/Flutter/guides/understanding_filters.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docusaurus/docs/Flutter/guides/understanding_filters.mdx b/docusaurus/docs/Flutter/guides/understanding_filters.mdx index 8a6fc925..d4ecf94c 100644 --- a/docusaurus/docs/Flutter/guides/understanding_filters.mdx +++ b/docusaurus/docs/Flutter/guides/understanding_filters.mdx @@ -85,7 +85,7 @@ Filter.notIn('members', [user.id]) #### Filter.query -Matches values by performing text search with the specified value. +The 'query' filter matches values by performing text search with the specified value. ```dart Filter.query('name', 'demo') From 1c846b4da7df6174171e44e157b129e00824817a Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 12 Aug 2021 18:57:04 +0530 Subject: [PATCH 098/121] Update docusaurus/docs/Flutter/guides/understanding_filters.mdx Co-authored-by: Gordon Hayes --- docusaurus/docs/Flutter/guides/understanding_filters.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docusaurus/docs/Flutter/guides/understanding_filters.mdx b/docusaurus/docs/Flutter/guides/understanding_filters.mdx index d4ecf94c..a88f371c 100644 --- a/docusaurus/docs/Flutter/guides/understanding_filters.mdx +++ b/docusaurus/docs/Flutter/guides/understanding_filters.mdx @@ -93,7 +93,7 @@ Filter.query('name', 'demo') #### Filter.autoComplete -Matches values with the specified prefix. +The 'autoComplete' filter matches values with the specified prefix. ```dart Filter.autoComplete('name', 'demo') From c3444fe4915b6344cad4b6416a56f36d0e2787f1 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 12 Aug 2021 18:57:10 +0530 Subject: [PATCH 099/121] Update docusaurus/docs/Flutter/guides/understanding_filters.mdx Co-authored-by: Gordon Hayes --- docusaurus/docs/Flutter/guides/understanding_filters.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docusaurus/docs/Flutter/guides/understanding_filters.mdx b/docusaurus/docs/Flutter/guides/understanding_filters.mdx index a88f371c..e7c288b4 100644 --- a/docusaurus/docs/Flutter/guides/understanding_filters.mdx +++ b/docusaurus/docs/Flutter/guides/understanding_filters.mdx @@ -101,7 +101,7 @@ Filter.autoComplete('name', 'demo') #### Filter.exists -Matches values that exist/don't exist based on the specified boolean value. +The 'exists' filter matches values that exist, or don't exist, based on the specified boolean value. ```dart Filter.exists('name', true) From a928bea12504168c935d899c503dfcdd29490d54 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 12 Aug 2021 19:00:54 +0530 Subject: [PATCH 100/121] added new guide --- docusaurus/docs/Flutter/guides/understanding_filters.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docusaurus/docs/Flutter/guides/understanding_filters.mdx b/docusaurus/docs/Flutter/guides/understanding_filters.mdx index e7c288b4..9d169a80 100644 --- a/docusaurus/docs/Flutter/guides/understanding_filters.mdx +++ b/docusaurus/docs/Flutter/guides/understanding_filters.mdx @@ -109,7 +109,7 @@ Filter.exists('name', true) ### Group Queries -#### FilterOperator.and +#### Filter.and The 'and' operator combines multiple queries. @@ -120,7 +120,7 @@ final filter = Filter.and([ ]) ``` -#### FilterOperator.or +#### Filter.or Combines the provided filters and matches the values matched by at least one of the filters. @@ -131,7 +131,7 @@ final filter = Filter.or([ ]) ``` -#### FilterOperator.nor +#### Filter.nor Combines the provided filters and matches the values not matched by all the filters. From f097ef9913094e10d7ae44cea03c298a225b6bbd Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 13 Aug 2021 11:45:40 +0530 Subject: [PATCH 101/121] chore(ui): Apply review feedbacks. Signed-off-by: xsahil03x --- .../stream_chat_flutter/lib/src/extension.dart | 2 +- .../lib/src/message_input.dart | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/extension.dart b/packages/stream_chat_flutter/lib/src/extension.dart index 2780e1c8..19bf4523 100644 --- a/packages/stream_chat_flutter/lib/src/extension.dart +++ b/packages/stream_chat_flutter/lib/src/extension.dart @@ -48,7 +48,7 @@ extension PlatformFileX on PlatformFile { /// Extension on [InputDecoration] extension InputDecorationX on InputDecoration { - /// Merges this [AvatarThemeData] with the [other] + /// Merges this [InputDecoration] with the [other] InputDecoration merge(InputDecoration? other) { if (other == null) return this; return copyWith( diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index f5e4158a..f556024d 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -50,9 +50,10 @@ typedef MentionTileBuilder = Widget Function( Member member, ); -/// Widget builder for action button -/// [defaultActionButton] is the default [IconButton] configuration -/// Use [defaultActionButton.copyWith] to easily customize it +/// Widget builder for action button. +/// +/// [defaultActionButton] is the default [IconButton] configuration, +/// Use [defaultActionButton.copyWith] to easily customize it. typedef ActionButtonBuilder = Widget Function( BuildContext context, IconButton defaultActionButton, @@ -257,10 +258,16 @@ class MessageInput extends StatefulWidget { /// A callback for error reporting final ErrorListener? onError; - /// Builder for customizing attachment button. + /// Builder for customizing the attachment button. + /// + /// The builder contains the default [IconButton] that can be customized by + /// calling `.copyWith`. final ActionButtonBuilder? attachmentButtonBuilder; - /// Builder for customizing command button. + /// Builder for customizing the command button. + /// + /// The builder contains the default [IconButton] that can be customized by + /// calling `.copyWith`. final ActionButtonBuilder? commandButtonBuilder; @override From 7b7a65cd70c56888de3cb1506faa7a5027108f86 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 13 Aug 2021 11:50:07 +0530 Subject: [PATCH 102/121] fix(localization): Fix hindi translations. Signed-off-by: xsahil03x --- .../lib/src/stream_chat_localizations_hi.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart index 850e481b..7d7f54d2 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart @@ -197,7 +197,7 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { 'कार्रवाई पूरी नहीं की जा सकी.'; @override - String get replyLabel => 'रिप्लाई'; + String get replyLabel => 'जवाब दें'; @override String togglePinUnpinText({required bool pinned}) { @@ -224,7 +224,7 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { } @override - String get photosLabel => 'तस्वीरें'; + String get photosLabel => 'फ़ोटोज'; String _getDay(DateTime dateTime) { final now = DateTime.now(); @@ -250,10 +250,10 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { String get todayLabel => 'आज'; @override - String get yesterdayLabel => 'बिता हुआ कल'; + String get yesterdayLabel => 'कल'; @override - String get channelIsMutedText => 'चैनल मौन है'; + String get channelIsMutedText => 'चैनल म्यूट है'; @override String get noTitleText => 'कोई शीर्षक नहीं'; From 955b5f1c0b0829d949f9218921f229b814287976 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 13 Aug 2021 11:55:56 +0530 Subject: [PATCH 103/121] chore(localization): Update changelog. Signed-off-by: xsahil03x --- packages/stream_chat_localizations/CHANGELOG.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_localizations/CHANGELOG.md b/packages/stream_chat_localizations/CHANGELOG.md index 865741c4..0845fa05 100644 --- a/packages/stream_chat_localizations/CHANGELOG.md +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -1,6 +1,18 @@ ## Upcoming -* Added support for [Spanish](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart) locale. +✅ Added + +* Added support + for [Spanish](https://github.com/GetStream/stream-chat-flutter/blob/master/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart) + locale. + +🔄 Changed + +* Some of the `Hindi` translations have been updated/changed for better understanding. + - 'रिप्लाई' -> 'जवाब दें' + - 'तस्वीरें' -> 'फ़ोटोज' + - 'बिता हुआ कल' -> 'कल' + - 'चैनल मौन है' -> 'चैनल म्यूट है' ## 1.0.2 From f18c326cf44340b2c1e57ddabcec0c806b4f6236 Mon Sep 17 00:00:00 2001 From: Gordon Date: Fri, 13 Aug 2021 08:46:50 +0200 Subject: [PATCH 104/121] docs: update title Co-authored-by: Deven Joshi --- docusaurus/docs/Flutter/guides/adding_localization.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docusaurus/docs/Flutter/guides/adding_localization.mdx b/docusaurus/docs/Flutter/guides/adding_localization.mdx index 3cad5696..8ecc0cce 100644 --- a/docusaurus/docs/Flutter/guides/adding_localization.mdx +++ b/docusaurus/docs/Flutter/guides/adding_localization.mdx @@ -1,7 +1,7 @@ --- id: adding_localization sidebar_position: 2 -title: Adding Localization (i18n) +title: Adding Localization (l10n) / Internationalization (i18n) --- Adding Localization To UI Widgets From 03e9f2e5d14a4425de00b19542ff6c287e4b0631 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 13 Aug 2021 14:49:53 +0530 Subject: [PATCH 105/121] Update packages/stream_chat_flutter/lib/src/message_input.dart Co-authored-by: Gordon --- packages/stream_chat_flutter/lib/src/message_input.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index f556024d..4765bcc5 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -53,7 +53,7 @@ typedef MentionTileBuilder = Widget Function( /// Widget builder for action button. /// /// [defaultActionButton] is the default [IconButton] configuration, -/// Use [defaultActionButton.copyWith] to easily customize it. +/// use [defaultActionButton.copyWith] to easily customize it. typedef ActionButtonBuilder = Widget Function( BuildContext context, IconButton defaultActionButton, From 6c71350d395518e1b98ff0cbe25f0dbc8abc5651 Mon Sep 17 00:00:00 2001 From: Jia-Han Wu <60439733+jiahan-wu@users.noreply.github.com> Date: Fri, 13 Aug 2021 15:42:45 +0800 Subject: [PATCH 106/121] fix(ui): Fix a `MessageInput` bug. --- .../lib/src/message_input.dart | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 9431cd22..a9d6c18b 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -1869,15 +1869,14 @@ class MessageInputState extends State { } else if (fileType == DefaultAttachmentTypes.video) { pickedFile = await _imagePicker.pickVideo(source: ImageSource.camera); } - if (pickedFile == null) { - return; + if (pickedFile != null) { + final bytes = await pickedFile.readAsBytes(); + file = AttachmentFile( + size: bytes.length, + path: pickedFile.path, + bytes: bytes, + ); } - final bytes = await pickedFile.readAsBytes(); - file = AttachmentFile( - size: bytes.length, - path: pickedFile.path, - bytes: bytes, - ); } else { late FileType type; if (fileType == DefaultAttachmentTypes.image) { From 4a9301317ad99e191df581ab00e48349cdbfa978 Mon Sep 17 00:00:00 2001 From: Jia-Han Wu Date: Fri, 13 Aug 2021 18:14:17 +0800 Subject: [PATCH 107/121] Update CHANGELOG.md --- packages/stream_chat_flutter/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 5289ec30..100853bb 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -28,6 +28,10 @@ Here's the full naming breakdown: * `MessageTheme` is now `MessageThemeData` * `UserListViewTheme` is now `UserListViewThemeData` +🐞 Fixed + +- Fixed `MessageInput` textField behaviour + ## 2.1.2 🐞 Fixed From 1a28a91be7a7ebbf58eef64264945b4aa47333a3 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 13 Aug 2021 16:19:14 +0530 Subject: [PATCH 108/121] Update packages/stream_chat_flutter/CHANGELOG.md --- packages/stream_chat_flutter/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 100853bb..218843f2 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -30,7 +30,7 @@ Here's the full naming breakdown: 🐞 Fixed -- Fixed `MessageInput` textField behaviour +- Fixed `MessageInput` textField case where `input` is not enabled if the file picked from the camera is null. ## 2.1.2 From d56a6df3de096bfeb2dd53af9b76b00ce0b31000 Mon Sep 17 00:00:00 2001 From: Gordon Date: Fri, 13 Aug 2021 14:34:51 +0200 Subject: [PATCH 109/121] docs: fix example folder link --- packages/stream_chat_flutter_core/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter_core/README.md b/packages/stream_chat_flutter_core/README.md index 462498f6..e625aeeb 100644 --- a/packages/stream_chat_flutter_core/README.md +++ b/packages/stream_chat_flutter_core/README.md @@ -26,7 +26,7 @@ It teaches you how to use this SDK and also shows how to make frequently require ## Example App This repo includes a fully functional example app with setup instructions. -The example is available under the [example](https://github.com/GetStream/stream-chat-flutter-core/tree/master/example) folder. +The example is available under the [example](https://github.com/GetStream/stream-chat-flutter/tree/main/packages/stream_chat_flutter_core/example) folder. ## Add dependency Add this to your package's pubspec.yaml file, use the latest version [![Pub](https://img.shields.io/pub/v/stream_chat_flutter_core.svg)](https://pub.dartlang.org/packages/stream_chat_flutter_core) From 3e4d39308f883f58733f1023aae254698c29a6bf Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 16 Aug 2021 20:19:55 +0530 Subject: [PATCH 110/121] refactor(llc, ui): Make Streams non-nullable wherever possible. Signed-off-by: xsahil03x --- .../stream_chat/lib/src/client/channel.dart | 86 +++++++++---------- .../lib/src/channel_preview.dart | 2 +- .../lib/src/message_list_view.dart | 4 +- 3 files changed, 42 insertions(+), 50 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index b22a57cc..3285db75 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -153,9 +153,9 @@ class Channel { true; /// Returns true if the channel is muted, as a stream. - Stream? get isMutedStream => _client.state.currentUserStream + Stream get isMutedStream => _client.state.currentUserStream .map((event) => - event!.channelMutes.any((element) => element.channel.cid == cid) == + event?.channelMutes.any((element) => element.channel.cid == cid) == true) .distinct(); @@ -168,105 +168,97 @@ class Channel { /// Channel configuration. ChannelConfig? get config { _checkInitialized(); - return state?._channelState.channel?.config; + return state!._channelState.channel?.config; } /// Channel configuration as a stream. - Stream? get configStream { + Stream get configStream { _checkInitialized(); - return state?.channelStateStream.map((cs) => cs.channel?.config); + return state!.channelStateStream.map((cs) => cs.channel?.config); } /// Channel user creator. User? get createdBy { _checkInitialized(); - return state?._channelState.channel?.createdBy; + return state!._channelState.channel?.createdBy; } /// Channel user creator as a stream. - Stream? get createdByStream { + Stream get createdByStream { _checkInitialized(); - return state?.channelStateStream.map((cs) => cs.channel?.createdBy); + return state!.channelStateStream.map((cs) => cs.channel?.createdBy); } /// Channel frozen status. - bool? get frozen { + bool get frozen { _checkInitialized(); - return state?._channelState.channel?.frozen; + return state!._channelState.channel?.frozen == true; } /// Channel frozen status as a stream. - Stream? get frozenStream { + Stream get frozenStream { _checkInitialized(); - return state?.channelStateStream.map((cs) => cs.channel?.frozen); + return state!.channelStateStream.map((cs) => cs.channel?.frozen == true); } /// Channel creation date. DateTime? get createdAt { _checkInitialized(); - return state?._channelState.channel?.createdAt; + return state!._channelState.channel?.createdAt; } /// Channel creation date as a stream. - Stream? get createdAtStream { + Stream get createdAtStream { _checkInitialized(); - return state?.channelStateStream.map((cs) => cs.channel?.createdAt); + return state!.channelStateStream.map((cs) => cs.channel?.createdAt); } /// Channel last message date. DateTime? get lastMessageAt { _checkInitialized(); - - return state?._channelState.channel?.lastMessageAt; + return state!._channelState.channel?.lastMessageAt; } /// Channel last message date as a stream. - Stream? get lastMessageAtStream { + Stream get lastMessageAtStream { _checkInitialized(); - - return state?.channelStateStream.map((cs) => cs.channel?.lastMessageAt); + return state!.channelStateStream.map((cs) => cs.channel?.lastMessageAt); } /// Channel updated date. DateTime? get updatedAt { _checkInitialized(); - - return state?._channelState.channel?.updatedAt; + return state!._channelState.channel?.updatedAt; } /// Channel updated date as a stream. - Stream? get updatedAtStream { + Stream get updatedAtStream { _checkInitialized(); - - return state?.channelStateStream.map((cs) => cs.channel?.updatedAt); + return state!.channelStateStream.map((cs) => cs.channel?.updatedAt); } /// Channel deletion date. DateTime? get deletedAt { _checkInitialized(); - - return state?._channelState.channel?.deletedAt; + return state!._channelState.channel?.deletedAt; } /// Channel deletion date as a stream. - Stream? get deletedAtStream { + Stream get deletedAtStream { _checkInitialized(); - - return state?.channelStateStream.map((cs) => cs.channel?.deletedAt); + return state!.channelStateStream.map((cs) => cs.channel?.deletedAt); } /// Channel member count. int? get memberCount { _checkInitialized(); - - return state?._channelState.channel?.memberCount; + return state!._channelState.channel?.memberCount; } /// Channel member count as a stream. - Stream? get memberCountStream { + Stream get memberCountStream { _checkInitialized(); - - return state?.channelStateStream.map((cs) => cs.channel?.memberCount); + return state!.channelStateStream.map((cs) => cs.channel?.memberCount); } /// Channel id. @@ -281,7 +273,7 @@ class Channel { /// Channel team. String? get team { _checkInitialized(); - return state?._channelState.channel?.team; + return state!._channelState.channel?.team; } /// Channel extra data. @@ -294,7 +286,7 @@ class Channel { } /// Channel extra data as a stream. - Stream> get extraDataStream { + Stream> get extraDataStream { _checkInitialized(); return state!.channelStateStream.map( (cs) => cs.channel?.extraData ?? _extraData, @@ -1728,9 +1720,9 @@ class ChannelClientState { (event) { final readList = List.from(_channelState.read); final userReadIndex = - read?.indexWhere((r) => r.user.id == event.user!.id); + read.indexWhere((r) => r.user.id == event.user!.id); - if (userReadIndex != null && userReadIndex != -1) { + if (userReadIndex != -1) { final userRead = readList.removeAt(userReadIndex); if (userRead.user.id == _channel._client.state.currentUser!.id) { unreadCount = 0; @@ -1751,15 +1743,15 @@ class ChannelClientState { List get messages => _channelState.messages; /// Channel message list as a stream. - Stream?> get messagesStream => channelStateStream + Stream> get messagesStream => channelStateStream .map((cs) => cs.messages) .distinct(const ListEquality().equals); /// Channel pinned message list. - List? get pinnedMessages => _channelState.pinnedMessages.toList(); + List get pinnedMessages => _channelState.pinnedMessages.toList(); /// Channel pinned message list as a stream. - Stream?> get pinnedMessagesStream => + Stream> get pinnedMessagesStream => channelStateStream.map((cs) => cs.pinnedMessages.toList()); /// Get channel last message. @@ -1768,8 +1760,8 @@ class ChannelClientState { : null; /// Get channel last message. - Stream get lastMessageStream => messagesStream - .map((event) => event?.isNotEmpty == true ? event!.last : null); + Stream get lastMessageStream => + messagesStream.map((event) => event.isNotEmpty ? event.last : null); /// Channel members list. List get members => _channelState.members @@ -1806,10 +1798,10 @@ class ChannelClientState { ); /// Channel read list. - List? get read => _channelState.read; + List get read => _channelState.read; /// Channel read list as a stream. - Stream?> get readStream => channelStateStream.map((cs) => cs.read); + Stream> get readStream => channelStateStream.map((cs) => cs.read); final BehaviorSubject _unreadCountController = BehaviorSubject.seeded(0); @@ -2060,7 +2052,7 @@ class ChannelClientState { .toList(); updateChannelState(_channelState.copyWith( - pinnedMessages: pinnedMessages!.where(_pinIsValid()).toList(), + pinnedMessages: pinnedMessages.where(_pinIsValid()).toList(), messages: expiredMessages, )); } diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index 333ee886..e01cd008 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -132,7 +132,7 @@ class ChannelPreview extends StatelessWidget { message: lastMessage!, size: channelPreviewTheme.indicatorIconSize, isMessageRead: channel.state!.read - ?.where((element) => + .where((element) => element.user.id != channel .client.state.currentUser!.id) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index a3830966..54634257 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -915,7 +915,7 @@ class _MessageListViewState extends State { } final channel = streamChannel!.channel; - final readList = channel.state?.read?.where((read) { + final readList = channel.state?.read.where((read) { if (read.user.id == userId) return false; return read.lastRead.isAfter(message.createdAt) || read.lastRead.isAtSameMomentAs(message.createdAt); @@ -1204,7 +1204,7 @@ class _MessageListViewState extends State { builder: (_) => BetterStreamBuilder( stream: streamChannel!.channel.state!.messagesStream.map( (messages) => - messages!.firstWhere((m) => m.id == message.id)), + messages.firstWhere((m) => m.id == message.id)), initialData: message, builder: (_, data) => StreamChannel( channel: streamChannel!.channel, From b4329786e8682a680c26980a6dc4cc173dadf73d Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 17 Aug 2021 10:00:59 +0200 Subject: [PATCH 111/121] fix(ui): date dividers in not reversed message list view --- .../lib/src/message_list_view.dart | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index a3830966..82697640 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -505,8 +505,14 @@ class _MessageListViewState extends State { if (i == 1 || i == itemCount - 4) return const Offstage(); - final message = messages[i - 1]; - final nextMessage = messages[i - 2]; + late final Message message, nextMessage; + if (widget.reverse) { + message = messages[i - 1]; + nextMessage = messages[i - 2]; + } else { + message = messages[i - 2]; + nextMessage = messages[i - 1]; + } if (!Jiffy(message.createdAt.toLocal()).isSame( nextMessage.createdAt.toLocal(), Units.DAY, @@ -636,8 +642,7 @@ class _MessageListViewState extends State { } Positioned _buildFloatingDateDivider(int itemCount) => Positioned( - top: widget.reverse ? 20 : null, - bottom: widget.reverse ? null : 20, + top: 20, left: 0, right: 0, child: BetterStreamBuilder>( @@ -647,16 +652,27 @@ class _MessageListViewState extends State { if (a == null || b == null) { return false; } - final aTop = _getTopElementIndex(a); - final bTop = _getTopElementIndex(b); - return aTop == bTop; + if (widget.reverse) { + final aTop = _getTopElementIndex(a); + final bTop = _getTopElementIndex(b); + return aTop == bTop; + } else { + final aBottom = _getBottomElementIndex(a); + final bBottom = _getBottomElementIndex(b); + return aBottom == bBottom; + } }, builder: (context, values) { if (values.isEmpty || messages.isEmpty) { return const Offstage(); } - final index = _getTopElementIndex(values); + late final int? index; + if (widget.reverse) { + index = _getTopElementIndex(values); + } else { + index = _getBottomElementIndex(values); + } if (index == null || index <= 2 || index >= itemCount - 3) { return const Offstage(); @@ -685,6 +701,15 @@ class _MessageListViewState extends State { .index; } + int? _getBottomElementIndex(Iterable values) { + final inView = values.where((position) => position.itemLeadingEdge < 1); + if (inView.isEmpty) return null; + return inView + .reduce((min, position) => + position.itemLeadingEdge < min.itemLeadingEdge ? position : min) + .index; + } + Widget _buildScrollToBottom() => StreamBuilder>( stream: Rx.combineLatest2( streamChannel!.channel.state!.isUpToDateStream.distinct(), From 79b2d91902b75a3235cde2507cf0c84f060a9999 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 17 Aug 2021 10:01:50 +0200 Subject: [PATCH 112/121] chore(ui): update changelog --- packages/stream_chat_flutter/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index add2c3cf..9138d952 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -45,6 +45,7 @@ breakdown: 🐞 Fixed - Fixed `MessageInput` textField case where `input` is not enabled if the file picked from the camera is null. +- Fixed date dividers in not reversed `MessageListView`. ## 2.1.2 From 607ad40443b818a61afd2acf0c5b1f496e1cc245 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 17 Aug 2021 10:32:56 +0200 Subject: [PATCH 113/121] fix(core): `ChannelListView` pagination on refresh --- packages/stream_chat_flutter_core/lib/src/channels_bloc.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart index 46aaccee..6687eef5 100644 --- a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart @@ -106,6 +106,9 @@ class ChannelsBlocState extends State final client = _streamChatCoreState!.client; final clear = paginationParams.offset == 0; + if (clear && _paginationEnded) { + _paginationEnded = false; + } if ((!clear && _paginationEnded) || _queryChannelsLoadingController.value == true) { From 6fb8c8731e2d6ba36f94d2856140f0b97828870a Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 17 Aug 2021 14:46:44 +0530 Subject: [PATCH 114/121] fix(ui): fix test Signed-off-by: xsahil03x --- packages/stream_chat_flutter/test/src/channel_preview_test.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/stream_chat_flutter/test/src/channel_preview_test.dart b/packages/stream_chat_flutter/test/src/channel_preview_test.dart index 7d0c549e..d6f3a5ea 100644 --- a/packages/stream_chat_flutter/test/src/channel_preview_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_preview_test.dart @@ -21,6 +21,8 @@ void main() { when(() => clientState.currentUser).thenReturn(user); when(() => clientState.currentUserStream) .thenAnswer((_) => Stream.value(user)); + when(() => channel.lastMessageAtStream) + .thenAnswer((_) => Stream.value(lastMessageAt)); when(() => channel.lastMessageAt).thenReturn(lastMessageAt); when(() => channel.state).thenReturn(channelState); when(() => channel.client).thenReturn(client); From d0f5f4af8322067d131ef5fc43dec76fdd882978 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 17 Aug 2021 11:19:02 +0200 Subject: [PATCH 115/121] fix format --- packages/stream_chat_flutter/lib/src/message_list_view.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 54634257..d7a49051 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -1203,8 +1203,7 @@ class _MessageListViewState extends State { MaterialPageRoute( builder: (_) => BetterStreamBuilder( stream: streamChannel!.channel.state!.messagesStream.map( - (messages) => - messages.firstWhere((m) => m.id == message.id)), + (messages) => messages.firstWhere((m) => m.id == message.id)), initialData: message, builder: (_, data) => StreamChannel( channel: streamChannel!.channel, From ad4be0e9d639ed51f9f358a88ec0a97657facb84 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 17 Aug 2021 11:26:16 +0200 Subject: [PATCH 116/121] update changelog --- packages/stream_chat_flutter_core/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index ef24e401..65280674 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,8 @@ +## Upcoming + +🐞 Fixed +- [#612](https://github.com/GetStream/stream-chat-flutter/issues/612) `ChannelListView` pagination doesn't work after refresh + ## 2.1.1 - Updated llc dependency From 7cdc2b98e51529e754cd287ddb00a123530403d2 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 17 Aug 2021 11:59:45 +0200 Subject: [PATCH 117/121] Update packages/stream_chat_flutter/CHANGELOG.md Co-authored-by: Sahil Kumar --- packages/stream_chat_flutter/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 9138d952..b21acc7a 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -45,7 +45,7 @@ breakdown: 🐞 Fixed - Fixed `MessageInput` textField case where `input` is not enabled if the file picked from the camera is null. -- Fixed date dividers in not reversed `MessageListView`. +- Fixed date dividers position/alignment in non reversed `MessageListView`. ## 2.1.2 From 56bd3b5b50ec6e461ab8bc5221c9bd3db67a1afe Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 17 Aug 2021 12:34:38 +0200 Subject: [PATCH 118/121] fix(core): always show date divider --- .../lib/src/message_list_view.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 82697640..8d83ab9d 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -667,7 +667,7 @@ class _MessageListViewState extends State { return const Offstage(); } - late final int? index; + int? index; if (widget.reverse) { index = _getTopElementIndex(values); } else { @@ -675,7 +675,14 @@ class _MessageListViewState extends State { } if (index == null || index <= 2 || index >= itemCount - 3) { - return const Offstage(); + if (index == null) { + return const Offstage(); + } + if (widget.reverse) { + index = itemCount - 4; + } else { + index = 2; + } } final message = messages[index - 2]; From b2eb51317f3abc2b18bd4655359ac788a385c85b Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 17 Aug 2021 16:17:19 +0530 Subject: [PATCH 119/121] refactor(ui): early return `Offstage` in case index is null. Signed-off-by: xsahil03x --- .../stream_chat_flutter/lib/src/message_list_view.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 8d83ab9d..cd9c4b45 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -674,10 +674,9 @@ class _MessageListViewState extends State { index = _getBottomElementIndex(values); } - if (index == null || index <= 2 || index >= itemCount - 3) { - if (index == null) { - return const Offstage(); - } + if (index == null) return const Offstage(); + + if (index <= 2 || index >= itemCount - 3) { if (widget.reverse) { index = itemCount - 4; } else { From 49fcaf40f5fa0d229aa38f6a4a92a20fb3a6a0dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Braun?= Date: Tue, 17 Aug 2021 20:15:52 +0200 Subject: [PATCH 120/121] Fix sample app link in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6758b189..62358d57 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ This repository contains code for our [Dart](https://dart.dev/) and [Flutter](ht Stream allows developers to rapidly deploy scalable feeds and chat messaging with an industry leading 99.999% uptime SLA guarantee. ## Sample apps and demos -Our team maintains a dedicated repository for fully-fledged sample applications and demos. Consider checking out [GetStream/flutter-samples](https://github.com/GetStream/flutter-samples) to learn more or get started by looking at our latest [Stream Chat demo](https://github.com/GetStream/flutter-samples/tree/main/stream_chat_v1). +Our team maintains a dedicated repository for fully-fledged sample applications and demos. Consider checking out [GetStream/flutter-samples](https://github.com/GetStream/flutter-samples) to learn more or get started by looking at our latest [Stream Chat demo](https://github.com/GetStream/flutter-samples/tree/main/packages/stream_chat_v1). ## Free for Makers From 84fccf2c63fb9b4ba364dc7254a6938099fe41fd Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 18 Aug 2021 12:07:27 +0200 Subject: [PATCH 121/121] fix(ui): revert wrapping ChannelPreview in a DecoratedBox because of Slidable --- .../lib/src/channel_list_view.dart | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index eadbed59..d6cd454b 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -584,11 +584,16 @@ class _ChannelListViewState extends State { ), ], child: widget.channelPreviewBuilder?.call(context, channel) ?? - ChannelPreview( - onLongPress: widget.onChannelLongPress, - channel: channel, - onImageTap: () => widget.onImageTap?.call(channel), - onTap: (channel) => onTap(channel, widget.channelWidget), + DecoratedBox( + decoration: BoxDecoration( + color: chatThemeData.channelListViewTheme.backgroundColor, + ), + child: ChannelPreview( + onLongPress: widget.onChannelLongPress, + channel: channel, + onImageTap: () => widget.onImageTap?.call(channel), + onTap: (channel) => onTap(channel, widget.channelWidget), + ), ), ), );