chromium/ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/create_tab_group_mediator.mm

// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/create_tab_group_mediator.h"

#import <memory>

#import "base/check.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/scoped_multi_source_observation.h"
#import "base/strings/sys_string_conversions.h"
#import "components/tab_groups/tab_group_color.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/browser/browser_list_factory.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/model/web_state_list/browser_util.h"
#import "ios/chrome/browser/shared/model/web_state_list/tab_group.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list_observer.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list_observer_bridge.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/group_tab_info.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/create_tab_group_mediator_delegate.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/tab_group_creation_consumer.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_group_item.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_group_utils.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_switcher_item.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_utils.h"
#import "ios/chrome/browser/ui/tab_switcher/web_state_tab_switcher_item.h"
#import "ios/web/public/web_state_id.h"

@interface CreateTabGroupMediator () <WebStateListObserving>
@end

@implementation CreateTabGroupMediator {
  // Consumer of the tab group creator;
  __weak id<TabGroupCreationConsumer> _consumer;
  // List of tabs to add to the tab group.
  std::set<web::WebStateID> _identifiers;
  // Web state list where the tab group belong.
  WebStateList* _webStateList;
  // Tab group to edit.
  const TabGroup* _tabGroup;
  // Array of all pictures of the group.
  NSMutableArray<GroupTabInfo*>* _tabGroupInfos;
  // Item to fetch pictures.
  TabGroupItem* _groupItem;
  // Source browser. Only set when creating a new group, not when editing an
  // existing one.
  Browser* _browser;
  // Observers for WebStateList. Only set when editing an existing group,
  // when creating a new one.
  std::unique_ptr<WebStateListObserverBridge> _webStateListObserverBridge;
  std::unique_ptr<
      base::ScopedMultiSourceObservation<WebStateList, WebStateListObserver>>
      _scopedWebStateListObservation;
}

- (instancetype)
    initTabGroupCreationWithConsumer:(id<TabGroupCreationConsumer>)consumer
                        selectedTabs:(std::set<web::WebStateID>&)identifiers
                             browser:(Browser*)browser {
  CHECK(IsTabGroupInGridEnabled())
      << "You should not be able to create a tab group outside the Tab Groups "
         "experiment.";
  self = [super init];
  if (self) {
    CHECK(consumer);
    CHECK(!identifiers.empty()) << "Cannot create an empty tab group.";
    CHECK(browser);
    _identifiers = identifiers;

    _browser = browser;
    _webStateList = browser->GetWebStateList();
    _consumer = consumer;
    [_consumer setDefaultGroupColor:TabGroup::DefaultColorForNewTabGroup(
                                        _webStateList)];

    ChromeBrowserState* browserState = browser->GetBrowserState();
    BrowserList* browserList =
        BrowserListFactory::GetForBrowserState(browserState);

    _tabGroupInfos = [[NSMutableArray alloc] init];

    NSUInteger numberOfRequestedImages = 0;
    for (web::WebStateID identifier : identifiers) {
      if (numberOfRequestedImages >= 7) {
        break;
      }
      WebStateList* currentWebStateList = _webStateList;
      // TODO(crbug.com/333032676): Replace this with the appropriate helper
      // once it exists.
      int index = GetWebStateIndex(
          _webStateList, WebStateSearchCriteria{.identifier = identifier});
      if (index == WebStateList::kInvalidIndex) {
        // The user is creating a group from a long press on search result. Tab
        // search can display all tabs from the same profile at the same time.
        // The selected tab is currently in a different web state list (inactive
        // tab, or tab from another window).
        Browser* selectedTabBrowser = GetBrowserForTabWithId(
            browserList, identifier, browserState->IsOffTheRecord());
        CHECK(browser);
        currentWebStateList = selectedTabBrowser->GetWebStateList();
        index =
            GetWebStateIndex(currentWebStateList,
                             WebStateSearchCriteria{.identifier = identifier});
      }

      __weak CreateTabGroupMediator* weakSelf = self;
      [TabGroupUtils
          fetchTabGroupInfoFromWebState:currentWebStateList->GetWebStateAt(
                                            index)
                             completion:^(GroupTabInfo* info) {
                               [weakSelf addInfo:info];
                               [weakSelf updateConsumer];
                             }];
      numberOfRequestedImages++;
    }
  }
  return self;
}

- (instancetype)initTabGroupEditionWithConsumer:
                    (id<TabGroupCreationConsumer>)consumer
                                       tabGroup:(const TabGroup*)tabGroup
                                   webStateList:(WebStateList*)webStateList {
  CHECK(IsTabGroupInGridEnabled())
      << "You should not be able to create a tab group outside the Tab Groups "
         "experiment.";
  self = [super init];
  if (self) {
    CHECK(consumer);
    CHECK(tabGroup);
    CHECK(webStateList);
    _consumer = consumer;
    _tabGroup = tabGroup;
    _webStateList = webStateList;
    // Observe the WebStateList in the case the group disappears.
    _webStateListObserverBridge =
        std::make_unique<WebStateListObserverBridge>(self);
    _scopedWebStateListObservation = std::make_unique<
        base::ScopedMultiSourceObservation<WebStateList, WebStateListObserver>>(
        _webStateListObserverBridge.get());
    _scopedWebStateListObservation->AddObservation(_webStateList);
    _groupItem = [[TabGroupItem alloc] initWithTabGroup:_tabGroup
                                           webStateList:_webStateList];
    __weak CreateTabGroupMediator* weakSelf = self;
    [_groupItem fetchGroupTabInfos:^(TabGroupItem* item,
                                     NSArray<GroupTabInfo*>* groupTabInfos) {
      [weakSelf setGroupTabInfos:groupTabInfos];
      [weakSelf updateConsumer];
    }];

    // Do not use the helper to get the following values as the title helper do
    // not return nil but the number of tabs. In this case, we want nil so it do
    // not display anything.
    tab_groups::TabGroupVisualData visualData = _tabGroup->visual_data();
    [_consumer setDefaultGroupColor:visualData.color()];
    [_consumer setGroupTitle:base::SysUTF16ToNSString(visualData.title())];
  }
  return self;
}

- (void)disconnect {
  if (_tabGroup) {
    _scopedWebStateListObservation->RemoveAllObservations();
    _scopedWebStateListObservation.reset();
    _webStateListObserverBridge.reset();
    _webStateList = nullptr;
  }
}

#pragma mark - TabGroupCreationMutator

// TODO(crbug.com/40942154): Rename the function to better match what it does.
- (void)createNewGroupWithTitle:(NSString*)title
                          color:(tab_groups::TabGroupColorId)colorID
                     completion:(void (^)())completion {
  tab_groups::TabGroupVisualData visualData =
      tab_groups::TabGroupVisualData(base::SysNSStringToUTF16(title), colorID);
  if (_tabGroup) {
    base::RecordAction(
        base::UserMetricsAction("MobileTabGroupUserUpdatedGroup"));
    if (![_tabGroup->GetRawTitle() isEqualToString:title]) {
      base::RecordAction(
          base::UserMetricsAction("MobileTabGroupUserUpdatedGroupName"));
    }
    if (![_tabGroup->GetColor()
            isEqual:TabGroup::ColorForTabGroupColorId(colorID)]) {
      base::RecordAction(
          base::UserMetricsAction("MobileTabGroupUserUpdatedGroupColor"));
    }
    _webStateList->UpdateGroupVisualData(_tabGroup, visualData);
  } else {
    base::RecordAction(
        base::UserMetricsAction("MobileTabGroupUserCreatedNewGroup"));
    std::set<int> tabIndexes;
    for (web::WebStateID identifier : _identifiers) {
      int index = GetWebStateIndex(_webStateList, WebStateSearchCriteria{
                                                      .identifier = identifier,
                                                  });
      if (index == WebStateList::kInvalidIndex) {
        index = _webStateList->count();
        MoveTabToBrowser(identifier, _browser, index);
      }
      tabIndexes.insert(index);
    }
    if (!tabIndexes.empty()) {
      _webStateList->CreateGroup(tabIndexes, visualData,
                                 tab_groups::TabGroupId::GenerateNew());
    }
  }
  completion();
}

#pragma mark - WebStateListObserving

- (void)didChangeWebStateList:(WebStateList*)webStateList
                       change:(const WebStateListChange&)change
                       status:(const WebStateListStatus&)status {
  CHECK_EQ(_webStateList, webStateList);
  switch (change.type()) {
    case WebStateListChange::Type::kGroupVisualDataUpdate: {
      const WebStateListChangeGroupVisualDataUpdate& visualDataUpdate =
          change.As<WebStateListChangeGroupVisualDataUpdate>();
      if (_tabGroup == visualDataUpdate.updated_group()) {
        // Dismiss the editor.
        [self.delegate
            createTabGroupMediatorEditedGroupWasExternallyMutated:self];
      }
      break;
    }
    case WebStateListChange::Type::kGroupDelete: {
      const WebStateListChangeGroupDelete& deletion =
          change.As<WebStateListChangeGroupDelete>();
      if (_tabGroup == deletion.deleted_group()) {
        // Dismiss the editor.
        [self.delegate
            createTabGroupMediatorEditedGroupWasExternallyMutated:self];
      }
      break;
    }
    default:
      // No-op.
      break;
  }
}

#pragma mark - Private helpers

// Adds the given info to the GroupTabInfo array.
- (void)addInfo:(GroupTabInfo*)info {
  [_tabGroupInfos addObject:info];
}

// Sets the GroupTabInfo array with `tabGroupInfos`.
- (void)setGroupTabInfos:(NSArray<GroupTabInfo*>*)tabGroupInfos {
  _tabGroupInfos = [[NSMutableArray alloc] initWithArray:tabGroupInfos];
}

// Sends to the consumer the needed pictures and the number of items to display
// it properly.
- (void)updateConsumer {
  NSInteger numberOfItem =
      _tabGroup ? _tabGroup->range().count() : _identifiers.size();
  [_consumer setTabGroupInfos:_tabGroupInfos
        numberOfSelectedItems:numberOfItem];
}

@end