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

// Copyright 2024 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/tab_groups_panel_mediator.h"

#import <memory>

#import "base/memory/raw_ptr.h"
#import "base/memory/weak_ptr.h"
#import "base/metrics/user_metrics.h"
#import "base/metrics/user_metrics_action.h"
#import "base/scoped_observation.h"
#import "base/strings/sys_string_conversions.h"
#import "components/saved_tab_groups/saved_tab_group.h"
#import "components/saved_tab_groups/string_utils.h"
#import "components/tab_groups/tab_group_color.h"
#import "ios/chrome/browser/favicon/model/favicon_loader.h"
#import "ios/chrome/browser/saved_tab_groups/model/ios_tab_group_sync_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/public/commands/tab_grid_commands.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/grid/grid_toolbars_mutator.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/tab_group_sync_service_observer_bridge.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/tab_groups_panel_consumer.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/tab_groups_panel_item.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/tab_groups_panel_item_data.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/tab_groups/tab_groups_panel_mediator_delegate.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/toolbars/tab_grid_toolbars_configuration.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_grid/toolbars/tab_grid_toolbars_grid_delegate.h"
#import "ios/chrome/common/ui/favicon/favicon_attributes.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "ui/gfx/favicon_size.h"
#import "ui/gfx/image/image.h"

using tab_groups::utils::GetLocalTabGroupInfo;
using tab_groups::utils::LocalTabGroupInfo;

namespace {

using ScopedTabGroupSyncObservation =
    base::ScopedObservation<tab_groups::TabGroupSyncService,
                            tab_groups::TabGroupSyncService::Observer>;

// Comparator for groups by creation date.
bool CompareGroupByCreationDate(const tab_groups::SavedTabGroup& a,
                                const tab_groups::SavedTabGroup& b) {
  return a.creation_time_windows_epoch_micros() >
         b.creation_time_windows_epoch_micros();
}

// Converts a vector of `SavedTabGroup`s into an array of `TabGroupsPanelItem`s.
NSArray<TabGroupsPanelItem*>* CreateItems(
    std::vector<tab_groups::SavedTabGroup> groups) {
  // Sort groups by creation date.
  std::sort(groups.begin(), groups.end(), CompareGroupByCreationDate);

  NSMutableArray<TabGroupsPanelItem*>* items = [[NSMutableArray alloc] init];
  for (const auto& group : groups) {
    TabGroupsPanelItem* item = [[TabGroupsPanelItem alloc] init];
    item.savedTabGroupID = group.saved_guid();
    [items addObject:item];
  }
  return items;
}

// Returns a user-friendly localized string representing the duration since the
// creation date.
NSString* CreationText(base::Time creation_date) {
  return base::SysUTF16ToNSString(tab_groups::LocalizedElapsedTimeSinceCreation(
      base::Time::Now() - creation_date));
}

}  // namespace

@interface TabGroupsPanelMediator () <TabGridToolbarsGridDelegate,
                                      TabGroupSyncServiceObserverDelegate>
@end

@implementation TabGroupsPanelMediator {
  // The service to observe.
  raw_ptr<tab_groups::TabGroupSyncService> _tabGroupSyncService;
  // The bridge between the service C++ observer and this Objective-C class.
  std::unique_ptr<TabGroupSyncServiceObserverBridge> _syncServiceObserver;
  std::unique_ptr<ScopedTabGroupSyncObservation> _scopedSyncServiceObservation;
  // Whether the service was fully initialized.
  bool _serviceInitialized;
  // The regular WebStateList, to check if there are tabs to go back to when
  // pressing the Done button.
  base::WeakPtr<WebStateList> _regularWebStateList;
  // The object to retrieve tabs favicons.
  raw_ptr<FaviconLoader> _faviconLoader;
  // Whether this screen is disabled by policy.
  BOOL _isDisabled;
  // Whether this screen is selected in the TabGrid.
  BOOL _selectedGrid;
  // A list of Browsers.
  BrowserList* _browserList;
}

- (instancetype)initWithTabGroupSyncService:
                    (tab_groups::TabGroupSyncService*)tabGroupSyncService
                        regularWebStateList:(WebStateList*)regularWebStateList
                              faviconLoader:(FaviconLoader*)faviconLoader
                           disabledByPolicy:(BOOL)disabled
                                browserList:(BrowserList*)browserList {
  self = [super init];
  if (self) {
    _tabGroupSyncService = tabGroupSyncService;
    _syncServiceObserver =
        std::make_unique<TabGroupSyncServiceObserverBridge>(self);
    _scopedSyncServiceObservation =
        std::make_unique<ScopedTabGroupSyncObservation>(
            _syncServiceObserver.get());
    _scopedSyncServiceObservation->Observe(_tabGroupSyncService);
    _regularWebStateList = regularWebStateList->AsWeakPtr();
    _faviconLoader = faviconLoader;
    _isDisabled = disabled;
    _browserList = browserList;
  }
  return self;
}

- (void)setConsumer:(id<TabGroupsPanelConsumer>)consumer {
  _consumer = consumer;
  if (_consumer) {
    [self populateItemsFromService];
  }
}

- (void)deleteSyncedTabGroup:(const base::Uuid&)syncID {
  const auto group = _tabGroupSyncService->GetGroup(syncID);
  if (!group) {
    return;
  }

  LocalTabGroupInfo tabGroupInfo = GetLocalTabGroupInfo(_browserList, *group);
  if (tabGroupInfo.tab_group) {
    // Delete the group and tabs in the group locally. It automatically updates
    // the tab group sync service.
    CloseAllWebStatesInGroup(*tabGroupInfo.web_state_list,
                             tabGroupInfo.tab_group,
                             WebStateList::CLOSE_USER_ACTION);
  } else {
    // The group doesn't exist locally. Delete the group from the tab group
    // sync service.
    _tabGroupSyncService->RemoveGroup(syncID);
  }
}

- (void)disconnect {
  _consumer = nil;
  _scopedSyncServiceObservation.reset();
  _syncServiceObserver.reset();
  _tabGroupSyncService = nullptr;
  _regularWebStateList = nullptr;
}

#pragma mark TabGridPageMutator

- (void)currentlySelectedGrid:(BOOL)selected {
  _selectedGrid = selected;

  if (selected) {
    base::RecordAction(base::UserMetricsAction("MobileTabGridSelectTabGroups"));

    [self configureToolbarsButtons];
  }
}

- (void)setPageAsActive {
  NOTREACHED() << "Should not be called in Tab Groups.";
}

#pragma mark TabGridToolbarsGridDelegate

- (void)closeAllButtonTapped:(id)sender {
  NOTREACHED() << "Should not be called in Tab Groups.";
}

- (void)doneButtonTapped:(id)sender {
  base::RecordAction(base::UserMetricsAction("MobileTabGridDone"));
  [self.tabGridHandler exitTabGrid];
}

- (void)newTabButtonTapped:(id)sender {
  NOTREACHED() << "Should not be called in Tab Groups.";
}

- (void)selectAllButtonTapped:(id)sender {
  NOTREACHED() << "Should not be called in Tab Groups.";
}

- (void)searchButtonTapped:(id)sender {
  NOTREACHED() << "Should not be called in Tab Groups.";
}

- (void)cancelSearchButtonTapped:(id)sender {
  NOTREACHED() << "Should not be called in Tab Groups.";
}

- (void)closeSelectedTabs:(id)sender {
  NOTREACHED() << "Should not be called in Tab Groups.";
}

- (void)shareSelectedTabs:(id)sender {
  NOTREACHED() << "Should not be called in Tab Groups.";
}

- (void)selectTabsButtonTapped:(id)sender {
  NOTREACHED() << "Should not be called in Tab Groups.";
}

#pragma mark TabGroupsPanelItemDataSource

- (TabGroupsPanelItemData*)dataForItem:(TabGroupsPanelItem*)item {
  const auto group = _tabGroupSyncService->GetGroup(item.savedTabGroupID);
  if (!group) {
    return nil;
  }

  // Gather the item data.
  TabGroupsPanelItemData* itemData = [[TabGroupsPanelItemData alloc] init];
  const auto title = group->title();
  const auto numberOfTabs = group->saved_tabs().size();
  if (title.length() > 0) {
    itemData.title = base::SysUTF16ToNSString(title);
  } else {
    itemData.title = l10n_util::GetPluralNSStringF(
        IDS_IOS_TAB_GROUP_TABS_NUMBER, numberOfTabs);
  }
  itemData.color = TabGroup::ColorForTabGroupColorId(group->color());
  itemData.creationText =
      CreationText(group->creation_time_windows_epoch_micros());
  itemData.numberOfTabs = static_cast<NSUInteger>(numberOfTabs);

  return itemData;
}

- (void)fetchFaviconForItem:(TabGroupsPanelItem*)item
                      index:(int)index
                 completion:(void (^)(UIImage*))completion {
  const auto group = _tabGroupSyncService->GetGroup(item.savedTabGroupID);
  if (!group) {
    return;
  }
  const auto saved_tabs = group->saved_tabs();
  if (static_cast<size_t>(index) >= saved_tabs.size() || index < 0) {
    return;
  }

  const auto saved_tab = saved_tabs[index];
  _faviconLoader->FaviconForPageUrlOrHost(
      saved_tab.url(), gfx::kFaviconSize, ^(FaviconAttributes* attributes) {
        // Pass only the non-default image.
        if (!attributes.usesDefaultImage) {
          completion(attributes.faviconImage);
        }
      });
}

#pragma mark TabGroupsPanelMutator

- (void)selectTabGroupsPanelItem:(TabGroupsPanelItem*)item {
  [self.delegate tabGroupsPanelMediator:self
                    openGroupWithSyncID:item.savedTabGroupID];
}

- (void)deleteTabGroupsPanelItem:(TabGroupsPanelItem*)item
                      sourceView:(UIView*)sourceView {
  [self.delegate tabGroupsPanelMediator:self
       showDeleteConfirmationWithSyncID:item.savedTabGroupID
                             sourceView:sourceView];
}

#pragma mark TabGroupSyncServiceObserverDelegate

- (void)tabGroupSyncServiceInitialized {
  _serviceInitialized = true;
  [self populateItemsFromService];
}

- (void)tabGroupSyncServiceTabGroupAdded:(const tab_groups::SavedTabGroup&)group
                              fromSource:(tab_groups::TriggerSource)source {
  [self populateItemsFromService];
}

- (void)tabGroupSyncServiceTabGroupUpdated:
            (const tab_groups::SavedTabGroup&)group
                                fromSource:(tab_groups::TriggerSource)source {
  [self reconfigureGroup:group];
}

- (void)tabGroupSyncServiceLocalTabGroupRemoved:
            (const tab_groups::LocalTabGroupID&)localID
                                     fromSource:
                                         (tab_groups::TriggerSource)source {
  // No-op. Only respond to Saved Tab Group Removed event.
}

- (void)tabGroupSyncServiceSavedTabGroupRemoved:(const base::Uuid&)syncID
                                     fromSource:
                                         (tab_groups::TriggerSource)source {
  [self populateItemsFromService];
}

#pragma mark Private

// Creates and send a tab grid toolbar configuration with button that should be
// displayed when Tab Groups is selected.
- (void)configureToolbarsButtons {
  if (!_selectedGrid) {
    return;
  }
  // Start to configure the delegate, so configured buttons will depend on the
  // correct delegate.
  [self.toolbarsMutator setToolbarsButtonsDelegate:self];

  if (_isDisabled) {
    [self.toolbarsMutator
        setToolbarConfiguration:
            [TabGridToolbarsConfiguration
                disabledConfigurationForPage:TabGridPageTabGroups]];
    return;
  }

  TabGridToolbarsConfiguration* toolbarsConfiguration =
      [[TabGridToolbarsConfiguration alloc] initWithPage:TabGridPageTabGroups];
  // Done button is enabled if there is at least one Regular tab.
  toolbarsConfiguration.doneButton =
      _regularWebStateList && !_regularWebStateList->empty();
  [self.toolbarsMutator setToolbarConfiguration:toolbarsConfiguration];
}

// Reads the TabGroupSyncService data, prepares it, and feeds it to the
// consumer.
- (void)populateItemsFromService {
  if (_serviceInitialized) {
    [_consumer populateItems:CreateItems(_tabGroupSyncService->GetAllGroups())];
  }
}

// Tells the consumer to reload the given group.
- (void)reconfigureGroup:(const tab_groups::SavedTabGroup&)group {
  if (_serviceInitialized) {
    TabGroupsPanelItem* item = [[TabGroupsPanelItem alloc] init];
    item.savedTabGroupID = group.saved_guid();
    [_consumer reconfigureItem:item];
  }
}

@end