chatview 3.0.0 copy "chatview: ^3.0.0" to clipboard
chatview: ^3.0.0 copied to clipboard

A Flutter package that allows you to integrate Chat View with highly customization options.

example/lib/main.dart

import 'package:chatview/chatview.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:intl/intl.dart';

import 'data.dart';
import 'example_two/example_two_list_screen.dart';
import 'models/chat_list_theme.dart';
import 'models/chatview_theme.dart';
import 'values/colors.dart';
import 'values/icons.dart';

void main() {
  runApp(const Example());
}

class Example extends StatelessWidget {
  const Example({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Chat UI Demo',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primaryColor: AppColors.uiOnePurple,
        colorScheme: ColorScheme.fromSwatch(accentColor: AppColors.uiOnePurple),
      ),
      darkTheme: ThemeData(
        primaryColor: AppColors.uiOnePurple,
        colorScheme: ColorScheme.fromSwatch(accentColor: AppColors.uiOnePurple),
      ),
      home: const ExampleOneListScreen(),
    );
  }
}

class ExampleOneListScreen extends StatefulWidget {
  const ExampleOneListScreen({super.key});

  @override
  State<ExampleOneListScreen> createState() => _ExampleOneListScreenState();
}

class _ExampleOneListScreenState extends State<ExampleOneListScreen> {
  ChatListTheme _theme = ChatListTheme.uiOneLight;
  bool _isDarkTheme = false;

  final _searchController = TextEditingController();

  final _chatListController = ChatListController(
    initialChatList: Data.getChatList(),
    scrollController: ScrollController(),
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: _theme.backgroundColor,
      body: SafeArea(
        child: ChatList(
          controller: _chatListController,
          backgroundColor: _theme.backgroundColor,
          header: _headerWidget(),
          appbar: ChatListAppBar(
            backgroundColor: _theme.backgroundColor,
            centerTitle: false,
            scrolledUnderElevation: 0,
            titleText: 'ChatList',
            titleTextStyle: TextStyle(
              fontSize: 20,
              color: _theme.textColor,
              fontWeight: FontWeight.bold,
            ),
            actions: [
              SvgPicture.asset(
                AppIcons.chatDiscoveryAi,
                colorFilter: ColorFilter.mode(
                  _theme.iconColor,
                  BlendMode.srcIn,
                ),
              ),
              const SizedBox(width: 16),
              SvgPicture.asset(
                AppIcons.createPen,
                colorFilter: ColorFilter.mode(
                  _theme.iconColor,
                  BlendMode.srcIn,
                ),
              ),
              IconButton(
                onPressed: _onThemeIconTap,
                icon: Icon(
                  _isDarkTheme ? Icons.light_mode : Icons.dark_mode,
                  color: _theme.iconColor,
                ),
              ),
            ],
          ),
          searchConfig: SearchConfig(
            textEditingController: _searchController,
            hintText: 'Ask Meta AI or search',
            hintStyle: TextStyle(
              fontSize: 16.4,
              color: _theme.searchText,
              fontWeight: FontWeight.w400,
            ),
            textStyle: TextStyle(
              fontSize: 16.4,
              color: _theme.textColor,
              fontWeight: FontWeight.w400,
            ),
            textFieldBackgroundColor: _theme.searchBg,
            borderRadius: const BorderRadius.all(Radius.circular(30)),
            prefixIcon: SizedBox.square(
              dimension: 48,
              child: Align(child: SvgPicture.asset(AppIcons.ai, width: 24)),
            ),
            clearIcon: Icon(Icons.clear, color: _theme.iconColor),
            contentPadding: const EdgeInsets.symmetric(
              vertical: 0,
              horizontal: 16,
            ),
            padding: const EdgeInsets.symmetric(horizontal: 16),
            onSearch: (value) {
              if (value.isEmpty) {
                _chatListController.clearSearch();
                return null;
              }

              List<ChatListItem> chats =
                  _chatListController.chatListMap.values.toList();

              final list = chats
                  .where((chat) =>
                      chat.name.toLowerCase().contains(value.toLowerCase()))
                  .toList();
              return list;
            },
          ),
          menuConfig: ChatMenuConfig(
            deleteCallback: (chat) => _chatListController.removeChat(chat.id),
            muteStatusCallback: (result) => _chatListController.updateChat(
              result.chat.id,
              (previousChat) => previousChat.copyWith(
                settings: previousChat.settings.copyWith(
                  muteStatus: result.status,
                ),
              ),
            ),
            pinStatusCallback: (result) => _chatListController.updateChat(
              result.chat.id,
              (previousChat) => previousChat.copyWith(
                settings: previousChat.settings.copyWith(
                  pinStatus: result.status,
                ),
              ),
            ),
          ),
          tileConfig: ListTileConfig(
            showUserActiveStatusIndicator: false,
            trailingBuilder: (chat) => _customTrailingWidget(chat),
            userNameBuilder: (chat) => _customUserNameWidget(chat),
            lastMessageTileBuilder: (chat) => _customLastMessageTile(chat),
            onTap: (chat) => Navigator.push(
              context,
              MaterialPageRoute(
                builder: (context) => ExampleOneChatScreen(chat: chat),
              ),
            ),
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
            userAvatarConfig: UserAvatarConfig(
              radius: 26,
              backgroundColor: _theme.secondaryBg,
            ),
            typingStatusConfig: TypingStatusConfig(
              textBuilder: (chat) => 'Typing...',
              textStyle: TextStyle(
                fontSize: 13,
                color: _theme.lastMessageText,
                fontStyle: FontStyle.italic,
              ),
            ),
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    _chatListController.dispose();
    _searchController.dispose();
    super.dispose();
  }

  void _onThemeIconTap() {
    setState(() {
      if (_isDarkTheme) {
        _theme = ChatListTheme.uiOneLight;
        _isDarkTheme = false;
      } else {
        _theme = ChatListTheme.uiOneDark;
        _isDarkTheme = true;
      }
    });
  }

  Widget _customTrailingWidget(ChatListItem chat) {
    final highlight = (chat.unreadCount ?? 0) > 0;
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        if (highlight) ...[
          const CircleAvatar(
            radius: 4,
            backgroundColor: AppColors.uiOneUnreadCountDot,
          ),
          const SizedBox(width: 12),
        ],
        SvgPicture.asset(
          AppIcons.camera2,
          colorFilter: ColorFilter.mode(
            highlight ? _theme.iconColor : AppColors.uiOneDarkGrey,
            BlendMode.srcIn,
          ),
        ),
      ],
    );
  }

  Widget _headerWidget() {
    return Column(
      children: [
        const SizedBox(height: 16),
        GestureDetector(
          onTap: () => Navigator.pushReplacement(
            context,
            MaterialPageRoute(
              builder: (context) => const ExampleTwoListScreen(),
            ),
          ),
          child: Text(
            '✨ Check out another UI',
            style: TextStyle(
              color: _theme.textColor,
              shadows: [
                Shadow(
                  color: _isDarkTheme ? Colors.white54 : Colors.black54,
                  offset: const Offset(0, -1),
                  blurRadius: 1,
                ),
              ],
              decorationColor: _theme.textColor,
              decoration: TextDecoration.underline,
            ),
          ),
        ),
        Padding(
          padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
          child: Row(
            children: [
              Expanded(
                child: Text(
                  'Messages',
                  maxLines: 1,
                  style: TextStyle(
                    fontSize: 16,
                    color: _theme.textColor,
                    fontWeight: FontWeight.bold,
                  ),
                  overflow: TextOverflow.ellipsis,
                ),
              ),
              Text(
                'Requests',
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
                style: TextStyle(color: _theme.searchText),
              ),
            ],
          ),
        ),
      ],
    );
  }

  Widget _customUserNameWidget(ChatListItem chat) {
    final highlightText = (chat.unreadCount ?? 0) > 0;
    return Row(
      children: [
        Flexible(
          child: Text(
            chat.name,
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
            style: TextStyle(
              fontSize: 14,
              color: highlightText ? _theme.textColor : _theme.lastMessageText,
              fontWeight: highlightText ? FontWeight.w500 : FontWeight.normal,
            ),
          ),
        ),
        if (chat.settings.pinStatus.isPinned) ...[
          const SizedBox(width: 6),
          SvgPicture.asset(
            AppIcons.pinned,
            width: 16,
            height: 16,
            colorFilter: ColorFilter.mode(
              _theme.lastMessageText,
              BlendMode.srcIn,
            ),
          ),
        ],
      ],
    );
  }

  Widget _customLastMessageTile(ChatListItem chat) {
    final message = chat.lastMessage;
    final unreadCount = chat.unreadCount ?? 0;
    final highlightText = unreadCount > 0;
    if (message == null) {
      return const SizedBox.shrink();
    }

    String prefix = switch (message.status) {
      MessageStatus.read => 'Seen',
      MessageStatus.delivered => 'Sent',
      MessageStatus.undelivered => 'Failed to send.',
      MessageStatus.pending => 'Sending...',
    };

    final showDisplayMessage = prefix == 'Seen' || prefix == 'Sent';

    final String display;
    if (highlightText && unreadCount != 1) {
      display = '$unreadCount new messages';
    } else if (!showDisplayMessage) {
      display = prefix;
    } else if (message.sentBy == 'me') {
      display = message.createdAt.getTimestamp(prefix: prefix);
    } else {
      display = switch (message.messageType) {
        MessageType.image => '$prefix a Photo',
        MessageType.text => message.message,
        MessageType.voice => '$prefix a Audio',
        MessageType.custom => '$prefix a Message',
      };
    }

    return Row(
      children: [
        if (showDisplayMessage && message.messageType != MessageType.text) ...[
          switch (message.messageType) {
            MessageType.image => const Icon(Icons.photo, size: 14),
            MessageType.voice => const Icon(Icons.mic, size: 14),
            MessageType.text || MessageType.custom => const SizedBox.shrink(),
          },
          const SizedBox(width: 5),
        ],
        Flexible(
          child: Text(
            display,
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
            style: TextStyle(
              fontSize: 14,
              color: message.status.isUndelivered
                  ? Colors.red
                  : highlightText
                      ? _theme.textColor
                      : _theme.lastMessageText,
              fontWeight: highlightText ? FontWeight.w500 : FontWeight.normal,
            ),
          ),
        ),
        if (showDisplayMessage) ...[
          Text(
            ' · ${message.createdAt.getTimeAgo}',
            style: TextStyle(
              fontSize: 14,
              color: _theme.lastMessageText,
              fontWeight: FontWeight.normal,
            ),
          ),
        ],
        if (chat.settings.muteStatus.isMuted) ...[
          const SizedBox(width: 6),
          Icon(
            Icons.notifications_off_outlined,
            size: 16,
            color: _theme.lastMessageText,
          ),
        ],
      ],
    );
  }
}

class ExampleOneChatScreen extends StatefulWidget {
  const ExampleOneChatScreen({required this.chat, super.key});

  final ChatListItem chat;

  @override
  State<ExampleOneChatScreen> createState() => _ExampleOneChatScreenState();
}

class _ExampleOneChatScreenState extends State<ExampleOneChatScreen> {
  ChatViewTheme _theme = ChatViewTheme.uiOneLight;
  bool _isDarkTheme = false;
  bool _isTopPaginationCalled = false;
  bool _isBottomPaginationCalled = false;

  final ChatController _chatController = ChatController(
    initialMessageList: Data.getMessageList(),
    scrollController: ScrollController(),
    currentUser: Data.currentUser,
    otherUsers: Data.otherUsers,
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SafeArea(
        child: ChatView(
          chatController: _chatController,
          onSendTap: _onSendTap,
          isLastPage: () => _isTopPaginationCalled && _isBottomPaginationCalled,
          loadMoreData: (direction, message) async {
            if (direction.isNext) {
              if (_isBottomPaginationCalled) {
                return;
              }
              _isBottomPaginationCalled = true;
            } else if (direction.isPrevious) {
              if (_isTopPaginationCalled) {
                return;
              }
              _isTopPaginationCalled = true;
            }
            await Future.delayed(const Duration(seconds: 1));
            _chatController.loadMoreData(
              direction.isPrevious
                  ? [
                      Message(
                        id: DateTime.timestamp()
                            .subtract(const Duration(days: 30, minutes: 10))
                            .toIso8601String(),
                        message: "Long time no see!",
                        createdAt: DateTime.now()
                            .subtract(const Duration(days: 30, minutes: 10)),
                        sentBy: '2',
                        status: MessageStatus.read,
                      ),
                      Message(
                        id: DateTime.timestamp()
                            .subtract(const Duration(days: 30, minutes: 5))
                            .toIso8601String(),
                        message: "Indeed! I was about to ping you.",
                        createdAt: DateTime.now()
                            .subtract(const Duration(days: 30, minutes: 5)),
                        sentBy: '1',
                        status: MessageStatus.read,
                      ),
                    ]
                  : [
                      Message(
                        id: DateTime.now()
                            .subtract(const Duration(minutes: 1))
                            .toIso8601String(),
                        message: "How about a movie marathon?",
                        createdAt:
                            DateTime.now().subtract(const Duration(minutes: 1)),
                        sentBy: '2',
                        status: MessageStatus.read,
                      ),
                      Message(
                        id: DateTime.now().toIso8601String(),
                        message: "Sounds great! I'm in. 🎬",
                        createdAt: DateTime.now(),
                        sentBy: '1',
                        status: MessageStatus.read,
                      ),
                    ],
              direction: direction,
            );
          },
          featureActiveConfig: const FeatureActiveConfig(
            lastSeenAgoBuilderVisibility: true,
            receiptsBuilderVisibility: true,
            enableOtherUserName: false,
            enableScrollToBottomButton: true,
            enableOtherUserProfileAvatar: true,
            enablePagination: true,
          ),
          scrollToBottomButtonConfig: const ScrollToBottomButtonConfig(
            padding: EdgeInsets.only(bottom: 8, right: 12),
            insidePadding: EdgeInsets.all(10),
            alignment: ScrollButtonAlignment.right,
            icon: Icon(Icons.arrow_downward_rounded),
            border: Border.fromBorderSide(BorderSide.none),
            boxShadow: [
              BoxShadow(
                blurRadius: 8,
                offset: Offset(0, 4),
                color: Colors.black26,
              ),
            ],
          ),
          chatViewState: ChatViewState.hasMessages,
          typeIndicatorConfig: TypeIndicatorConfiguration(
            customIndicator: _customTypingIndicator(),
          ),
          appBar: ChatViewAppBar(
            elevation: 0,
            chatTitle: widget.chat.name,
            leading: IconButton(
              onPressed: Navigator.of(context).maybePop,
              icon: Icon(Icons.arrow_back_ios, color: _theme.iconColor),
            ),
            chatTitleTextStyle: TextStyle(
              fontSize: 18,
              color: _theme.titleColor,
              fontWeight: FontWeight.w600,
            ),
            profilePicture: widget.chat.imageUrl,
            backGroundColor: _theme.backgroundColor,
            userStatus: '@${widget.chat.name}',
            userStatusTextStyle: TextStyle(
              fontSize: 13,
              color: _theme.titleColor,
            ),
            actions: [
              IconButton(
                onPressed: () {},
                icon: SvgPicture.asset(
                  AppIcons.chatDiscoveryAi,
                  colorFilter: ColorFilter.mode(
                    _theme.iconColor,
                    BlendMode.srcIn,
                  ),
                ),
              ),
              IconButton(
                onPressed: () {},
                icon: SvgPicture.asset(
                  AppIcons.phone,
                  colorFilter: ColorFilter.mode(
                    _theme.iconColor,
                    BlendMode.srcIn,
                  ),
                ),
              ),
              IconButton(
                // Handle video call
                onPressed: () {},
                icon: SvgPicture.asset(
                  AppIcons.video,
                  colorFilter: ColorFilter.mode(
                    _theme.iconColor,
                    BlendMode.srcIn,
                  ),
                ),
              ),
              PopupMenuButton(
                icon: Icon(
                  Icons.more_vert_rounded,
                  color: _theme.iconColor,
                ),
                itemBuilder: (context) => [
                  const PopupMenuItem(
                    value: 'toggle_typing_indicator',
                    child: Text('Toggle TypingIndicator'),
                  ),
                  const PopupMenuItem(
                    value: 'simulate_message_receive',
                    child: Text('Simulate Message receive'),
                  ),
                  PopupMenuItem(
                    value: 'dark_theme',
                    child: Text(' ${_isDarkTheme ? 'Light' : 'Dark'} Mode'),
                  ),
                ],
                onSelected: (value) {
                  switch (value) {
                    case 'toggle_typing_indicator':
                      _showHideTypingIndicator();
                    case 'simulate_message_receive':
                      receiveMessage();
                    case 'dark_theme':
                      _onThemeIconTap();
                  }
                },
              ),
              const SizedBox(width: 12),
            ],
          ),
          chatBackgroundConfig: ChatBackgroundConfiguration(
            backgroundColor: _theme.backgroundColor,
            groupSeparatorBuilder: (separator) =>
                _customSeparatorWidget(separator),
          ),
          sendMessageConfig: SendMessageConfiguration(
            closeIconColor: _theme.iconColor,
            replyTitleColor: _theme.textColor,
            replyMessageColor: _theme.textColor,
            replyDialogColor: _theme.backgroundColor,
            defaultSendButtonColor: Colors.white,
            textFieldBackgroundColor: _theme.textField,
            voiceRecordingConfiguration: VoiceRecordingConfiguration(
              recorderIconColor: _theme.iconColor,
              waveStyle: WaveStyle(
                extendWaveform: true,
                showMiddleLine: false,
                waveColor: _theme.iconColor,
                durationLinesColor: AppColors.black20,
                backgroundColor: Colors.transparent,
                scaleFactor: 60,
                waveThickness: 3,
                spacing: 4,
              ),
            ),
            sendButtonStyle: IconButton.styleFrom(
              backgroundColor: AppColors.uiOnePurple,
              padding: const EdgeInsets.symmetric(horizontal: 16),
            ),
            textFieldConfig: TextFieldConfiguration(
              hintText: 'Message...',
              hideLeadingActionsOnType: false,
              onMessageTyping: (status) {
                /// Do with status
                debugPrint(status.toString());
              },
              compositionThresholdTime: const Duration(seconds: 1),
              textStyle: TextStyle(color: _theme.textColor),
              contentPadding: const EdgeInsets.symmetric(horizontal: 12),
              leadingActions: (context, controller) =>
                  controller.text.trim().isEmpty
                      ? [
                          CameraActionButton(
                            icon: const Icon(
                              Icons.camera_alt_rounded,
                              color: Colors.white,
                            ),
                            style: IconButton.styleFrom(
                              backgroundColor: AppColors.uiOnePurple,
                            ),
                            onPressed: (path, replyMessage) {
                              if (path?.isEmpty ?? true) return;
                              _chatController.addMessage(
                                Message(
                                  id: DateTime.now()
                                      .millisecondsSinceEpoch
                                      .toString(),
                                  message: path!,
                                  createdAt: DateTime.now(),
                                  messageType: MessageType.image,
                                  sentBy: _chatController.currentUser.id,
                                  replyMessage:
                                      replyMessage ?? const ReplyMessage(),
                                ),
                              );
                              _chatController.addMessage(
                                Message(
                                  message: controller.text,
                                  id: DateTime.now()
                                      .millisecondsSinceEpoch
                                      .toString(),
                                  createdAt: DateTime.now(),
                                  sentBy: _chatController.currentUser.id,
                                  replyMessage:
                                      replyMessage ?? const ReplyMessage(),
                                ),
                              );
                            },
                          ),
                        ]
                      : [
                          IconButton(
                            icon: const Icon(
                              Icons.search_rounded,
                              color: Colors.white,
                            ),
                            style: IconButton.styleFrom(
                              backgroundColor: AppColors.uiOnePurple,
                            ),
                            onPressed: () =>
                                ScaffoldMessenger.of(context).showSnackBar(
                              const SnackBar(
                                  content: Text('Search button pressed')),
                            ),
                          ),
                        ],
              trailingActions: (context, controller) => [
                GalleryActionButton(
                  icon: Icon(
                    Icons.photo_rounded,
                    size: 30,
                    color: _theme.iconColor,
                  ),
                  onPressed: (path, replyMessage) {
                    if (path?.isEmpty ?? true) return;
                    _chatController.addMessage(
                      Message(
                        id: DateTime.now().millisecondsSinceEpoch.toString(),
                        message: path!,
                        createdAt: DateTime.now(),
                        messageType: MessageType.image,
                        sentBy: _chatController.currentUser.id,
                        replyMessage: replyMessage ?? const ReplyMessage(),
                      ),
                    );
                  },
                ),
                EmojiPickerActionButton(
                  context: context,
                  onPressed: (emoji, replyMessage) {
                    if (emoji?.isEmpty ?? true) return;
                    controller.text = controller.text += emoji!;
                    _chatController.addMessage(
                      Message(
                        message: controller.text,
                        id: DateTime.now().millisecondsSinceEpoch.toString(),
                        createdAt: DateTime.now(),
                        sentBy: _chatController.currentUser.id,
                        replyMessage: replyMessage ?? const ReplyMessage(),
                      ),
                    );
                  },
                  icon: SvgPicture.asset(
                    AppIcons.sticker,
                    width: 30,
                    height: 30,
                    colorFilter: ColorFilter.mode(
                      _theme.iconColor,
                      BlendMode.srcIn,
                    ),
                  ),
                ),
                IconButton(
                  onPressed: () => ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(content: Text('Add attachment')),
                  ),
                  icon: Icon(
                    Icons.add_circle_outline_rounded,
                    size: 30,
                    color: _theme.iconColor,
                  ),
                ),
              ],
            ),
          ),
          chatBubbleConfig: ChatBubbleConfiguration(
            outgoingChatBubbleConfig: const ChatBubble(
              linkPreviewConfig: LinkPreviewConfiguration(
                backgroundColor: Colors.white12,
                linkStyle: TextStyle(
                  fontSize: 16,
                  color: Colors.white,
                ),
              ),
              color: AppColors.uiOnePurple,
              textStyle: TextStyle(color: Colors.white, fontSize: 16),
              receiptsWidgetConfig: ReceiptsWidgetConfig(
                showReceiptsIn: ShowReceiptsIn.lastMessage,
              ),
            ),
            inComingChatBubbleConfig: ChatBubble(
              linkPreviewConfig: LinkPreviewConfiguration(
                backgroundColor: Colors.white12,
                linkStyle: TextStyle(
                  fontSize: 16,
                  color: _theme.textColor,
                ),
              ),
              color: _theme.incomingBubble,
              textStyle: TextStyle(color: _theme.textColor, fontSize: 16),
              onMessageRead: (message) {
                /// send your message reciepts to the other client
                debugPrint('Message Read');
              },
            ),
          ),
          reactionPopupConfig: const ReactionPopupConfiguration(
            backgroundColor: Colors.white,
            shadow: BoxShadow(
              blurRadius: 8,
              offset: Offset(0, 4),
              color: Colors.black26,
            ),
          ),
          messageConfig: MessageConfiguration(
            voiceMessageConfig: VoiceMessageConfiguration(
              margin: EdgeInsets.zero,
              padding: EdgeInsets.zero,
              playIcon: (isMessageBySender) => Icon(
                Icons.play_arrow_rounded,
                size: 24,
                color: isMessageBySender ? Colors.white : _theme.iconColor,
              ),
              pauseIcon: (isMessageBySender) => Icon(
                Icons.pause_rounded,
                size: 24,
                color: isMessageBySender ? Colors.white : _theme.iconColor,
              ),
              inComingPlayerWaveStyle: PlayerWaveStyle(
                liveWaveColor: _theme.iconColor,
                fixedWaveColor: AppColors.black20,
                backgroundColor: Colors.transparent,
                scaleFactor: 60,
                waveThickness: 3,
                spacing: 4,
              ),
              outgoingPlayerWaveStyle: PlayerWaveStyle(
                liveWaveColor: _theme.iconColor,
                fixedWaveColor: AppColors.white20,
                backgroundColor: Colors.transparent,
                scaleFactor: 60,
                waveThickness: 3,
                spacing: 4,
              ),
            ),
            messageReactionConfig: MessageReactionConfiguration(
              backgroundColor: _theme.incomingBubble,
              borderColor: _theme.backgroundColor,
              borderWidth: 2,
              reactionsBottomSheetConfig: ReactionsBottomSheetConfiguration(
                backgroundColor: _theme.backgroundColor,
                reactedUserTextStyle: TextStyle(
                  color: _theme.textColor,
                ),
                reactionWidgetDecoration: BoxDecoration(
                  color: _theme.incomingBubble,
                  borderRadius: const BorderRadius.all(Radius.circular(12)),
                ),
              ),
            ),
            customMessageBuilder: (message) {
              if (message.message == 'Message unavailable') {
                return _buildUnavailableMessage(message);
              }
              return const SizedBox.shrink();
            },
            emojiMessageConfig: const EmojiMessageConfiguration(
              maxOutSideBubbleEmojis: 1,
            ),
          ),
          profileCircleConfig: const ProfileCircleConfiguration(
            padding: EdgeInsets.only(right: 4),
            profileImageUrl: Data.profileImage,
          ),
          repliedMessageConfig: RepliedMessageConfiguration(
            backgroundColor: _theme.replyBg,
            verticalBarColor: _theme.verticalDivider,
            textStyle: TextStyle(color: _theme.replyText),
            replyTitleTextStyle: TextStyle(
              fontSize: 12,
              color: _theme.titleColor,
            ),
            loadOldReplyMessage: (id) async => {
              // Implement logic to fetch old replied message with surrounding context using message ID
            },
            repliedMsgAutoScrollConfig: RepliedMsgAutoScrollConfig(
              enableHighlightRepliedMsg: true,
              highlightScale: 1.1,
              highlightColor: Colors.grey.shade300,
            ),
          ),
          swipeToReplyConfig: SwipeToReplyConfiguration(
            replyIconColor: _theme.iconColor,
            replyIconBackgroundColor: _theme.backgroundColor,
            replyIconProgressRingColor: _theme.incomingBubble,
          ),
          replySuggestionsConfig: ReplySuggestionsConfig(
            itemConfig: SuggestionItemConfig(
              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
              decoration: BoxDecoration(
                color: _theme.incomingBubble,
                borderRadius: BorderRadius.circular(30),
                border: Border.all(color: _theme.incomingBubble),
              ),
              textStyle: TextStyle(color: _theme.textColor),
            ),
            onTap: (item) => _onSendTap(
              item.text,
              const ReplyMessage(),
              MessageType.text,
            ),
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    // ChatController should be disposed to avoid memory leaks
    _chatController.dispose();
    super.dispose();
  }

  void _showHideTypingIndicator() {
    _chatController.setTypingIndicator = !_chatController.showTypingIndicator;
  }

  void receiveMessage() async {
    _chatController.addMessage(
      Message(
        id: DateTime.now().toString(),
        message: 'I will schedule the meeting.',
        createdAt: DateTime.now(),
        sentBy: '2',
      ),
    );
    await Future.delayed(const Duration(milliseconds: 500));
    _chatController.addReplySuggestions([
      const SuggestionItemData(text: 'Thanks.'),
      const SuggestionItemData(text: 'Thank you very much.'),
      const SuggestionItemData(text: 'Great.')
    ]);
  }

  void _onSendTap(
    String message,
    ReplyMessage replyMessage,
    MessageType messageType,
  ) {
    final messageObj = Message(
      id: DateTime.now().toString(),
      createdAt: DateTime.now(),
      message: message,
      sentBy: _chatController.currentUser.id,
      replyMessage: replyMessage,
      messageType: messageType,
    );
    _chatController.addMessage(
      messageObj,
    );

    Future.delayed(const Duration(milliseconds: 300), () {
      final index = _chatController.initialMessageList.indexOf(messageObj);
      _chatController.initialMessageList[index].setStatus =
          MessageStatus.undelivered;
    });
    Future.delayed(const Duration(seconds: 1), () {
      final index = _chatController.initialMessageList.indexOf(messageObj);
      _chatController.initialMessageList[index].setStatus = MessageStatus.read;
    });
  }

  void _onThemeIconTap() {
    setState(() {
      if (_isDarkTheme) {
        _theme = ChatViewTheme.uiOneLight;
        _isDarkTheme = false;
      } else {
        _theme = ChatViewTheme.uiOneDark;
        _isDarkTheme = true;
      }
    });
  }

  Widget _buildUnavailableMessage(Message message) {
    return Container(
      constraints:
          BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.75),
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
      margin: EdgeInsets.fromLTRB(
          5, 0, 6, message.reaction.reactions.isNotEmpty ? 15 : 2),
      decoration: BoxDecoration(
        color: _theme.incomingBubble,
        borderRadius: const BorderRadius.all(Radius.circular(18)),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            message.message,
            style: TextStyle(
              fontSize: 16,
              color: _theme.textColor,
              fontWeight: FontWeight.w600,
            ),
          ),
          const SizedBox(height: 4),
          Text(
            'This content may have been deleted by its owner or hidden by their privacy settings.',
            style: TextStyle(color: _theme.textColor, fontSize: 15),
          ),
        ],
      ),
    );
  }

  Widget _customSeparatorWidget(String separator) {
    final date = DateTime.tryParse(separator);
    if (date == null) {
      return const SizedBox.shrink();
    }
    String separatorDate;
    final now = DateTime.now();
    if (date.day == now.day &&
        date.month == now.month &&
        date.year == now.year) {
      separatorDate = DateFormat('h:mm a').format(date);
    } else if (date.day == now.day - 1 &&
        date.month == now.month &&
        date.year == now.year) {
      separatorDate = 'Yesterday at ${DateFormat('h:mm a').format(date)}';
    } else if (date.isAfter(now.subtract(const Duration(days: 7)))) {
      separatorDate = DateFormat('EEE h:mm a').format(date);
    } else {
      separatorDate = DateFormat('d MMM AT h:mm a').format(date);
    }
    return Align(
      heightFactor: 2,
      child: Text(
        separatorDate.toUpperCase(),
        style: TextStyle(
          fontSize: 13,
          fontWeight: FontWeight.w500,
          color: _theme.titleColor,
        ),
      ),
    );
  }

  Widget _customTypingIndicator() {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        if (_chatController.otherUsers.firstOrNull?.profilePhoto
            case final image?) ...[
          CircleAvatar(
            radius: 16,
            backgroundImage: NetworkImage(image),
            backgroundColor: _theme.incomingBubble,
          ),
        ],
        Container(
          margin: const EdgeInsets.only(left: 8),
          decoration: BoxDecoration(
            color: _theme.incomingBubble,
            borderRadius: const BorderRadius.all(Radius.circular(30)),
          ),
          padding: const EdgeInsets.symmetric(
            vertical: 18,
            horizontal: 14,
          ),
          child: const Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              CircleAvatar(
                radius: 2.75,
                backgroundColor: AppColors.uiOneDarkGrey,
              ),
              SizedBox(width: 3),
              CircleAvatar(
                radius: 2.75,
                backgroundColor: AppColors.uiOneDarkGrey,
              ),
              SizedBox(width: 3),
              CircleAvatar(
                radius: 2.75,
                backgroundColor: AppColors.uiOneDarkGrey,
              ),
            ],
          ),
        ),
      ],
    );
  }
}

extension on DateTime {
  String get getTimeAgo {
    final now = DateTime.now();
    final diff = now.difference(this);

    if (diff.inSeconds < 60) return 'now';
    if (diff.inMinutes < 60) return '${diff.inMinutes}m';
    if (diff.inHours < 24) return '${diff.inHours}h';
    if (diff.inDays < 7) return '${diff.inDays}d';
    if (diff.inDays < 30) {
      final weeks = (diff.inDays / 7).floor();
      return '${weeks}w';
    }
    return now.year == year
        ? DateFormat('MMM d').format(this) // e.g. Aug 5
        : DateFormat('MMM d, y').format(this); // e.g. Aug 5, 2023;
  }

  String getTimestamp({required String prefix}) {
    String timeStamp;
    if (isNow) {
      timeStamp = 'just Now';
    } else {
      final difference = DateTime.now().difference(this);
      // if the difference is 1 day, show hours wise
      if (difference.inDays == 0) {
        if (difference.inHours > 0) {
          timeStamp = '${difference.inHours}h ago';
        } else if (difference.inMinutes > 0) {
          timeStamp = '${difference.inMinutes}m ago';
        } else {
          timeStamp = 'now';
        }
        // if less than 7 days, show days wise
      } else if (isLast7Days) {
        final day = DateFormat('EEEE').format(this);
        timeStamp = 'on $day';
      } else {
        timeStamp = '';
      }
    }
    return '$prefix $timeStamp';
  }

  bool get isNow {
    final now = DateTime.now();
    return year == now.year &&
        month == now.month &&
        day == now.day &&
        hour == now.hour &&
        minute == now.minute;
  }

  bool get isLast7Days {
    final now = DateTime.now();
    return now.difference(DateTime(year, month, day)).inDays < 7;
  }
}
634
likes
160
points
1.66k
downloads

Publisher

verified publishersimform.com

Weekly Downloads

A Flutter package that allows you to integrate Chat View with highly customization options.

Repository (GitHub)
View/report issues
Contributing

Documentation

API reference

License

MIT (license)

Dependencies

any_link_preview, audio_waveforms, cached_network_image, chatview_utils, emoji_picker_flutter, flutter, image_picker, intl, url_launcher

More

Packages that depend on chatview