build method

  1. @override
Widget build(
  1. BuildContext context
)
override

Describes the part of the user interface represented by this widget.

The framework calls this method when this widget is inserted into the tree in a given BuildContext and when the dependencies of this widget change (e.g., an InheritedWidget referenced by this widget changes). This method can potentially be called in every frame and should not have any side effects beyond building a widget.

The framework replaces the subtree below this widget with the widget returned by this method, either by updating the existing subtree or by removing the subtree and inflating a new subtree, depending on whether the widget returned by this method can update the root of the existing subtree, as determined by calling Widget.canUpdate.

Typically implementations return a newly created constellation of widgets that are configured with information from this widget's constructor and from the given BuildContext.

The given BuildContext contains information about the location in the tree at which this widget is being built. For example, the context provides the set of inherited widgets for this location in the tree. A given widget might be built with multiple different BuildContext arguments over time if the widget is moved around the tree or if the widget is inserted into the tree in multiple places at once.

The implementation of this method must only depend on:

If a widget's build method is to depend on anything else, use a StatefulWidget instead.

See also:

  • StatelessWidget, which contains the discussion on performance considerations.

Implementation

@override
Widget build(BuildContext context) {
  final style = Theme.of(context).style;

  return GetBuilder<ChatController>(
    key: const Key('ChatView'),
    init: ChatController(
      id,
      Get.find(),
      Get.find(),
      Get.find(),
      Get.find(),
      Get.find(),
      Get.find(),
      itemId: itemId,
      onContext: () => context,
    ),
    tag: id.val,
    global: !Get.isRegistered<ChatController>(tag: id.val),
    builder: (c) {
      // Opens [Routes.chatInfo] or [Routes.user] page basing on the
      // [Chat.isGroup] indicator.
      void onDetailsTap() {
        final Chat? chat = c.chat?.chat.value;
        if (chat != null) {
          if (chat.isGroup || chat.isMonolog) {
            router.chatInfo(chat.id, push: true);
          } else if (chat.members.isNotEmpty) {
            router.user(
              chat.members
                      .firstWhereOrNull((e) => e.user.id != c.me)
                      ?.user
                      .id ??
                  chat.members.first.user.id,
              push: true,
            );
          }
        }
      }

      return Obx(() {
        if (c.status.value.isEmpty) {
          return Scaffold(
            appBar: AppBar(),
            body: Center(child: Text('label_no_chat_found'.l10n)),
          );
        } else if (!c.status.value.isSuccess) {
          return Scaffold(
            appBar: const CustomAppBar(
              padding: EdgeInsets.only(left: 4, right: 20),
              leading: [StyledBackButton()],
            ),
            body: const Center(child: CustomProgressIndicator.primary()),
            bottomNavigationBar: _bottomBar(c, context),
          );
        }

        final bool isMonolog = c.chat!.chat.value.isMonolog;

        return CustomDropTarget(
          key: Key('ChatView_$id'),
          onPerformDrop: c.dropFiles,
          builder: (dragging) => GestureDetector(
            onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
            child: Stack(
              children: [
                Scaffold(
                  resizeToAvoidBottomInset: true,
                  appBar: CustomAppBar(
                    title: Obx(() {
                      if (c.searching.value) {
                        return Theme(
                          data: MessageFieldView.theme(context),
                          child: Padding(
                            padding: const EdgeInsets.symmetric(
                              horizontal: 10,
                            ),
                            child: Transform.translate(
                              offset: const Offset(0, 1),
                              child: ReactiveTextField(
                                key: const Key('SearchField'),
                                state: c.search,
                                hint: 'label_search'.l10n,
                                maxLines: 1,
                                filled: false,
                                dense: true,
                                padding: const EdgeInsets.symmetric(
                                  vertical: 8,
                                ),
                                style:
                                    style.fonts.medium.regular.onBackground,
                                onChanged: () {
                                  c.query.value = c.search.text;
                                  if (c.search.text.isEmpty) {
                                    c.switchToMessages();
                                  }
                                },
                              ),
                            ),
                          ),
                        );
                      }

                      return Row(
                        children: [
                          WidgetButton(
                            onPressed: onDetailsTap,
                            child: Center(
                              child: AvatarWidget.fromRxChat(
                                c.chat,
                                radius: AvatarRadius.medium,
                              ),
                            ),
                          ),
                          const SizedBox(width: 10),
                          Flexible(
                            child: InkWell(
                              splashFactory: NoSplash.splashFactory,
                              hoverColor: style.colors.transparent,
                              highlightColor: style.colors.transparent,
                              onTap: onDetailsTap,
                              child: DefaultTextStyle.merge(
                                maxLines: 1,
                                overflow: TextOverflow.ellipsis,
                                child: Column(
                                  mainAxisAlignment: MainAxisAlignment.center,
                                  crossAxisAlignment:
                                      CrossAxisAlignment.start,
                                  children: [
                                    Row(
                                      children: [
                                        Flexible(
                                          child: Obx(() {
                                            return Text(
                                              c.chat!.title,
                                              style: style
                                                  .fonts
                                                  .big
                                                  .regular
                                                  .onBackground,
                                              overflow: TextOverflow.ellipsis,
                                              maxLines: 1,
                                            );
                                          }),
                                        ),
                                        Obx(() {
                                          if (c.chat?.chat.value.muted ==
                                              null) {
                                            return const SizedBox();
                                          }

                                          return const Padding(
                                            padding: EdgeInsets.only(left: 5),
                                            child: SvgIcon(SvgIcons.muted),
                                          );
                                        }),
                                      ],
                                    ),
                                    if (!isMonolog)
                                      ChatSubtitle(c.chat!, c.me),
                                  ],
                                ),
                              ),
                            ),
                          ),
                          const SizedBox(width: 10),
                        ],
                      );
                    }),
                    padding: const EdgeInsets.only(left: 4),
                    leading: [
                      Obx(() {
                        if (c.searching.value) {
                          return const Padding(
                            padding: EdgeInsets.only(left: 16),
                            child: SvgIcon(SvgIcons.search),
                          );
                        }

                        return const StyledBackButton();
                      }),
                    ],
                    border:
                        (c.searching.value ||
                            c.search.isFocused.value == true ||
                            c.query.value?.isNotEmpty == true)
                        ? Border.all(color: style.colors.primary, width: 2)
                        : null,
                    actions: [
                      Obx(() {
                        if (c.searching.value) {
                          return WidgetButton(
                            onPressed: () {
                              if (c.searching.value) {
                                if (c.search.text.isNotEmpty) {
                                  c.search.clear();
                                  c.search.focus.requestFocus();
                                  c.switchToMessages();
                                } else {
                                  c.toggleSearch();
                                }
                              } else {
                                c.toggleSearch();
                              }
                            },
                            child: Padding(
                              padding: const EdgeInsets.fromLTRB(8, 8, 21, 8),
                              child: c.search.isEmpty.value
                                  ? const SvgIcon(SvgIcons.closePrimary)
                                  : const SvgIcon(SvgIcons.clearSearch),
                            ),
                          );
                        }

                        final bool blocked = c.chat?.blocked == true;
                        final bool inCall = c.chat?.inCall.value ?? false;

                        final List<Widget> children;

                        // Display the join/end call button, if [Chat] has an
                        // [OngoingCall] happening in it.
                        if (c.chat!.chat.value.ongoingCall != null) {
                          final Widget child;

                          if (inCall) {
                            child = Container(
                              key: const Key('Drop'),
                              height: 32,
                              width: 32,
                              decoration: BoxDecoration(
                                color: style.colors.danger,
                                shape: BoxShape.circle,
                              ),
                              child: const Center(
                                child: SvgIcon(SvgIcons.callEnd),
                              ),
                            );
                          } else {
                            child = Container(
                              key: const Key('Join'),
                              height: 32,
                              width: 32,
                              decoration: BoxDecoration(
                                color: style.colors.primary,
                                shape: BoxShape.circle,
                              ),
                              child: const Center(
                                child: SvgIcon(SvgIcons.callStart),
                              ),
                            );
                          }

                          children = [
                            AnimatedButton(
                              key: const Key('ActiveCallButton'),
                              onPressed: inCall ? c.dropCall : c.joinCall,
                              child: SafeAnimatedSwitcher(
                                duration: 300.milliseconds,
                                child: child,
                              ),
                            ),
                          ];
                        } else if (!blocked) {
                          children = [
                            if (c.callPosition == null ||
                                c.callPosition ==
                                    CallButtonsPosition.appBar) ...[
                              AnimatedButton(
                                onPressed: () => c.call(true),
                                child: const SvgIcon(SvgIcons.chatVideoCall),
                              ),
                              const SizedBox(width: 28),
                              AnimatedButton(
                                key: const Key('AudioCall'),
                                onPressed: () => c.call(false),
                                child: const SvgIcon(SvgIcons.chatAudioCall),
                              ),
                              const SizedBox(width: 10),
                            ],
                          ];
                        } else {
                          // [Chat]-dialog is blocked, therefore no call
                          // buttons should be displayed.
                          children = [];
                        }

                        return Row(
                          children: [
                            ...children,
                            Obx(() {
                              final bool muted =
                                  c.chat?.chat.value.muted != null;

                              final bool dialog =
                                  c.chat?.chat.value.isDialog == true;

                              final bool isLocal =
                                  c.chat?.chat.value.id.isLocal == true;

                              final bool monolog =
                                  c.chat?.chat.value.isMonolog == true;

                              final bool favorite =
                                  c.chat?.chat.value.favoritePosition != null;

                              // TODO: Uncomment, when contacts are
                              //       implemented.
                              // final bool contact =
                              //     c.user?.user.value.contacts.isNotEmpty ??
                              //         false;

                              final Widget child;

                              if (c.selecting.value) {
                                child = AnimatedButton(
                                  key: const Key('CancelSelecting'),
                                  onPressed: c.selecting.toggle,
                                  child: Container(
                                    padding: const EdgeInsets.only(left: 10),
                                    height: double.infinity,
                                    child: const Padding(
                                      padding: EdgeInsets.fromLTRB(
                                        10,
                                        0,
                                        21,
                                        0,
                                      ),
                                      child: SvgIcon(SvgIcons.closePrimary),
                                    ),
                                  ),
                                );
                              } else {
                                child = ContextMenuRegion(
                                  key: c.moreKey,
                                  selector: c.moreKey,
                                  alignment: Alignment.topRight,
                                  enablePrimaryTap: true,
                                  margin: const EdgeInsets.only(
                                    bottom: 4,
                                    right: 10,
                                  ),
                                  actions: [
                                    if (c.callPosition ==
                                        CallButtonsPosition.contextMenu) ...[
                                      ContextMenuButton(
                                        label: 'btn_audio_call'.l10n,
                                        onPressed: blocked || inCall
                                            ? null
                                            : () => c.call(false),
                                        trailing: SvgIcon(
                                          blocked || inCall
                                              ? SvgIcons.makeAudioCallDisabled
                                              : SvgIcons.makeAudioCall,
                                        ),
                                        inverted: const SvgIcon(
                                          SvgIcons.makeAudioCallWhite,
                                        ),
                                      ),
                                      ContextMenuButton(
                                        label: 'btn_video_call'.l10n,
                                        onPressed: blocked || inCall
                                            ? null
                                            : () => c.call(true),
                                        trailing: SvgIcon(
                                          blocked || inCall
                                              ? SvgIcons.makeVideoCallDisabled
                                              : SvgIcons.makeVideoCall,
                                        ),
                                        inverted: const SvgIcon(
                                          SvgIcons.makeVideoCallWhite,
                                        ),
                                      ),
                                    ],
                                    ContextMenuButton(
                                      key: const Key('SearchItemsButton'),
                                      label: 'label_search'.l10n,
                                      onPressed: c.toggleSearch,
                                      trailing: const SvgIcon(
                                        SvgIcons.search,
                                      ),
                                      inverted: const SvgIcon(
                                        SvgIcons.searchWhite,
                                      ),
                                    ),
                                    // TODO: Uncomment, when contacts are implemented.
                                    // if (dialog)
                                    //   ContextMenuButton(
                                    //     key: Key(
                                    //       contact
                                    //           ? 'DeleteFromContactsButton'
                                    //           : 'AddToContactsButton',
                                    //     ),
                                    //     label: contact
                                    //         ? 'btn_delete_from_contacts'.l10n
                                    //         : 'btn_add_to_contacts'.l10n,
                                    //     trailing: SvgIcon(
                                    //       contact
                                    //           ? SvgIcons.deleteContact
                                    //           : SvgIcons.addContact,
                                    //     ),
                                    //     inverted: SvgIcon(
                                    //       contact
                                    //           ? SvgIcons.deleteContactWhite
                                    //           : SvgIcons.addContactWhite,
                                    //     ),
                                    //     onPressed: contact
                                    //         ? () => _removeFromContacts(
                                    //               c,
                                    //               context,
                                    //             )
                                    //         : c.addToContacts,
                                    //   ),
                                    ContextMenuButton(
                                      key: Key(
                                        favorite
                                            ? 'UnfavoriteChatButton'
                                            : 'FavoriteChatButton',
                                      ),
                                      label: favorite
                                          ? 'btn_delete_from_favorites'.l10n
                                          : 'btn_add_to_favorites'.l10n,
                                      trailing: SvgIcon(
                                        favorite
                                            ? SvgIcons.favoriteSmall
                                            : SvgIcons.unfavoriteSmall,
                                      ),
                                      inverted: SvgIcon(
                                        favorite
                                            ? SvgIcons.favoriteSmallWhite
                                            : SvgIcons.unfavoriteSmallWhite,
                                      ),
                                      onPressed: favorite
                                          ? c.unfavoriteChat
                                          : c.favoriteChat,
                                    ),
                                    if (!isLocal) ...[
                                      if (!monolog)
                                        ContextMenuButton(
                                          key: Key(
                                            muted
                                                ? 'UnmuteChatButton'
                                                : 'MuteChatButton',
                                          ),
                                          label: muted
                                              ? PlatformUtils.isMobile
                                                    ? 'btn_unmute'.l10n
                                                    : 'btn_unmute_chat'.l10n
                                              : PlatformUtils.isMobile
                                              ? 'btn_mute'.l10n
                                              : 'btn_mute_chat'.l10n,
                                          trailing: SvgIcon(
                                            muted
                                                ? SvgIcons.unmuteSmall
                                                : SvgIcons.muteSmall,
                                          ),
                                          inverted: SvgIcon(
                                            muted
                                                ? SvgIcons.unmuteSmallWhite
                                                : SvgIcons.muteSmallWhite,
                                          ),
                                          onPressed: muted
                                              ? c.unmuteChat
                                              : c.muteChat,
                                        ),
                                      ContextMenuButton(
                                        key: const Key('ClearHistoryButton'),
                                        label: 'btn_clear_history'.l10n,
                                        trailing: const SvgIcon(
                                          SvgIcons.cleanHistory,
                                        ),
                                        inverted: const SvgIcon(
                                          SvgIcons.cleanHistoryWhite,
                                        ),
                                        onPressed: () =>
                                            _clearChat(c, context),
                                      ),
                                    ],
                                    if (!monolog && !dialog)
                                      ContextMenuButton(
                                        key: const Key('LeaveGroupButton'),
                                        label: 'btn_leave_group'.l10n,
                                        trailing: const SvgIcon(
                                          SvgIcons.leaveGroup,
                                        ),
                                        inverted: const SvgIcon(
                                          SvgIcons.leaveGroupWhite,
                                        ),
                                        onPressed: () =>
                                            _leaveGroup(c, context),
                                      ),
                                    if (!isLocal || monolog)
                                      ContextMenuButton(
                                        key: const Key('HideChatButton'),
                                        label: 'btn_delete_chat'.l10n,
                                        trailing: const SvgIcon(
                                          SvgIcons.delete19,
                                        ),
                                        inverted: const SvgIcon(
                                          SvgIcons.delete19White,
                                        ),
                                        onPressed: () =>
                                            _hideChat(c, context),
                                      ),
                                    if (dialog)
                                      ContextMenuButton(
                                        key: Key(
                                          blocked ? 'Unblock' : 'Block',
                                        ),
                                        label: blocked
                                            ? 'btn_unblock'.l10n
                                            : 'btn_block'.l10n,
                                        trailing: const SvgIcon(
                                          SvgIcons.block,
                                        ),
                                        inverted: const SvgIcon(
                                          SvgIcons.blockWhite,
                                        ),
                                        onPressed: blocked
                                            ? c.unblock
                                            : () => _blockUser(c, context),
                                      ),
                                    ContextMenuButton(
                                      label: 'btn_select_messages'.l10n,
                                      onPressed: c.selecting.toggle,
                                      trailing: const SvgIcon(
                                        SvgIcons.select,
                                      ),
                                      inverted: const SvgIcon(
                                        SvgIcons.selectWhite,
                                      ),
                                    ),
                                  ],
                                  child: Container(
                                    key: const Key('MoreButton'),
                                    padding: const EdgeInsets.only(
                                      left: 20,
                                      right: 21,
                                    ),
                                    height: double.infinity,
                                    child: const SvgIcon(SvgIcons.more),
                                  ),
                                );
                              }

                              return AnimatedButton(
                                child: SafeAnimatedSwitcher(
                                  duration: 250.milliseconds,
                                  child: child,
                                ),
                              );
                            }),
                          ],
                        );
                      }),
                    ],
                  ),
                  body: Stack(
                    children: [
                      // Required for the [Stack] to take [Scaffold]'s
                      // size.
                      IgnorePointer(
                        child: ObscuredMenuInterceptor(child: Container()),
                      ),
                      Obx(() {
                        final Widget child = FlutterListView(
                          key: const Key('MessagesList'),
                          controller: c.listController,
                          physics: c.isDraggingItem.value
                              ? const NeverScrollableScrollPhysics()
                              : const BouncingScrollPhysics(),
                          reverse: true,
                          delegate: FlutterListViewDelegate(
                            (context, i) => _listElement(context, c, i),
                            // ignore: invalid_use_of_protected_member
                            childCount: c.elements.value.length,
                            stickyAtTailer: true,
                            keepPosition: true,
                            keepPositionOffset: c.active.isTrue
                                ? c.keepPositionOffset.value
                                : 1,
                            onItemKey: (i) =>
                                c.elements.values.elementAt(i).id.toString(),
                            onItemSticky: (i) =>
                                c.elements.values.elementAt(i)
                                    is DateTimeElement,
                            initIndex: c.initIndex,
                            initOffset: c.initOffset,
                            initOffsetBasedOnBottom: true,
                            disableCacheItems: kDebugMode ? true : false,
                          ),
                        );

                        if (PlatformUtils.isMobile) {
                          if (!PlatformUtils.isWeb) {
                            return Scrollbar(
                              controller: c.listController,
                              child: child,
                            );
                          } else {
                            return child;
                          }
                        }

                        return ObscuredSelectionArea(
                          key: Key('${c.selecting.value}'),
                          onSelectionChanged: (a) => c.selection.value = a,
                          contextMenuBuilder: (_, __) => const SizedBox(),
                          selectionControls: EmptyTextSelectionControls(),
                          child: ObscuredMenuInterceptor(child: child),
                        );
                      }),
                      Obx(() {
                        if (c.searching.value) {
                          if (c.status.value.isLoadingMore) {
                            return const Center(
                              child: CustomProgressIndicator(),
                            );
                          }
                          // ignore: invalid_use_of_protected_member
                          else if (c.elements.value.isEmpty) {
                            return Center(
                              child: SystemInfoPrompt(
                                key: const Key('NoMessages'),
                                'label_no_messages'.l10n,
                              ),
                            );
                          }
                        }

                        if ((c.chat!.status.value.isSuccess ||
                                c.chat!.status.value.isEmpty) &&
                            c.chat!.messages.isEmpty) {
                          final Widget? welcome = _welcomeMessage(context, c);

                          if (welcome != null) {
                            return Center(
                              child: ListView(
                                shrinkWrap: true,
                                children: [
                                  Align(
                                    alignment: Alignment.centerLeft,
                                    child: ConstrainedBox(
                                      constraints: const BoxConstraints(
                                        maxWidth: 550,
                                      ),
                                      child: Padding(
                                        padding: const EdgeInsets.only(
                                          right: 48,
                                        ),
                                        child: welcome,
                                      ),
                                    ),
                                  ),
                                ],
                              ),
                            );
                          }

                          if (isMonolog) {
                            return Center(
                              key: const Key('NoMessages'),
                              child: NotesBlock(),
                            );
                          }

                          return Center(
                            child: SystemInfoPrompt(
                              key: const Key('NoMessages'),
                              'label_no_messages'.l10n,
                            ),
                          );
                        }

                        if (c.status.value.isLoading) {
                          return const Center(
                            child: CustomProgressIndicator(),
                          );
                        }

                        return const SizedBox();
                      }),
                      if (c.callPosition == CallButtonsPosition.top ||
                          c.callPosition == CallButtonsPosition.bottom)
                        Positioned(
                          top: c.callPosition == CallButtonsPosition.top
                              ? 8
                              : null,
                          bottom: c.callPosition == CallButtonsPosition.bottom
                              ? 8
                              : null,
                          right: 12,
                          child: Obx(() {
                            final bool inCall = c.chat?.inCall.value ?? false;

                            return Column(
                              mainAxisSize: MainAxisSize.min,
                              children: [
                                const SizedBox(height: 8),
                                CircleButton(
                                  inCall
                                      ? SvgIcons.chatAudioCallDisabled
                                      : SvgIcons.chatAudioCall,
                                  onPressed: inCall
                                      ? null
                                      : () => c.call(false),
                                ),
                                const SizedBox(height: 8),
                                CircleButton(
                                  inCall
                                      ? SvgIcons.chatVideoCallDisabled
                                      : SvgIcons.chatVideoCall,
                                  onPressed: inCall
                                      ? null
                                      : () => c.call(true),
                                ),
                                const SizedBox(height: 8),
                              ],
                            );
                          }),
                        ),
                    ],
                  ),
                  floatingActionButton: Obx(() {
                    return SizedBox(
                      width: 50,
                      height: 50,
                      child: SafeAnimatedSwitcher(
                        duration: 200.milliseconds,
                        child: c.canGoBack.isTrue
                            ? FloatingActionButton.small(
                                onPressed: c.animateToBack,
                                child: const Icon(Icons.arrow_upward),
                              )
                            : c.canGoDown.isTrue
                            ? FloatingActionButton.small(
                                onPressed: c.animateToBottom,
                                child: const Icon(Icons.arrow_downward),
                              )
                            : const SizedBox(),
                      ),
                    );
                  }),
                  bottomNavigationBar: _bottomBar(c, context),
                ),
                IgnorePointer(
                  child: SafeAnimatedSwitcher(
                    duration: 200.milliseconds,
                    child: dragging
                        ? Container(
                            color: style.colors.onBackgroundOpacity27,
                            child: Center(
                              child: AnimatedDelayedScale(
                                duration: const Duration(milliseconds: 300),
                                beginScale: 1,
                                endScale: 1.06,
                                child: ConditionalBackdropFilter(
                                  borderRadius: BorderRadius.circular(16),
                                  child: Container(
                                    decoration: BoxDecoration(
                                      borderRadius: BorderRadius.circular(16),
                                      color:
                                          style.colors.onBackgroundOpacity27,
                                    ),
                                    child: const Padding(
                                      padding: EdgeInsets.all(16),
                                      child: SvgIcon(SvgIcons.addBigger),
                                    ),
                                  ),
                                ),
                              ),
                            ),
                          )
                        : null,
                  ),
                ),
              ],
            ),
          ),
        );
      });
    },
  );
}