You are here

social_group.module in Open Social 10.0.x

The Social group module.

File

modules/social_features/social_group/social_group.module
View source
<?php

/**
 * @file
 * The Social group module.
 */
use Drupal\block\Entity\Block;
use Drupal\bootstrap\Bootstrap;
use Drupal\Component\Utility\Html;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultNeutral;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\group\Entity\GroupContentInterface;
use Drupal\group\Entity\GroupContentType;
use Drupal\group\Entity\GroupInterface;
use Drupal\group\Entity\GroupContent;
use Drupal\group\Entity\GroupType;
use Drupal\node\NodeInterface;
use Drupal\social_group\Controller\SocialGroupController;
use Drupal\social_group\Controller\SocialGroupListBuilder;
use Drupal\social_group\Element\SocialGroupEntityAutocomplete;
use Drupal\social_group\Entity\Access\SocialGroupAccessControlHandler;
use Drupal\social_group\Entity\Group;
use Drupal\social_group\Form\SocialGroupAddForm;
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\cache\CachePluginBase;
use Drupal\views\Plugin\views\query\QueryPluginBase;
use Drupal\views\Plugin\views\row\EntityRow;
use Drupal\Core\Url;
use Drupal\Core\Cache\Cache;
use Drupal\image\Entity\ImageStyle;
use Drupal\Core\Block\BlockPluginInterface;
use Drupal\user\Entity\Role;
use Drupal\user\Entity\User;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\social_group\GroupContentVisibilityUpdate;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\views_bulk_operations\ViewsBulkOperationsBatch;

/**
 * Implements hook_theme().
 */
function social_group_theme() {
  return [
    'group_settings_help' => [
      'variables' => [
        'group_type' => NULL,
        'join_method' => NULL,
        'allowed_visibility' => NULL,
      ],
    ],
  ];
}

/**
 * Implements hook_entity_type_alter().
 */
function social_group_entity_type_alter(array &$entity_types) {

  /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
  if (isset($entity_types['group'])) {
    $entity_types['group']
      ->setClass(Group::class)
      ->setListBuilderClass(SocialGroupListBuilder::class)
      ->setAccessClass(SocialGroupAccessControlHandler::class);
  }
}

/**
 * Prepares variables for group settings help text templates.
 *
 * Default template: group-settings-help.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - group_type: The group type.
 *   - join_method: The join methods.
 *   - allowed_visibility: The allowed visibilities.
 */
function template_preprocess_group_settings_help(array &$variables) {
}

/**
 * Check if a User is able to edit a Group's GroupType.
 *
 * @return bool
 *   TRUE if a user may edit a existings groups group type.
 */
function social_group_group_type_permission_check() {
  $user = \Drupal::currentUser();

  // Get the Group object from the route.
  $group = _social_group_get_current_group();

  // Check if we have a default visibility, if we don't it must be a custom
  // group type, we need to have a visibility in order to update group content.
  // please see hook_social_group_default_visibility_alter.
  if (\Drupal::service('social_group.helper_service')
    ->getDefaultGroupVisibility($group
    ->getGroupType()
    ->id()) === NULL) {
    return FALSE;
  }

  // Otherwise return true when we are able to edit the current group type.
  return $group instanceof GroupInterface && $user
    ->hasPermission('edit group types');
}

/**
 * Get group overview route.
 *
 * @return array
 *   An array with route name and parameters and group.
 */
function _social_group_get_overview_route(GroupInterface $group) {
  $route = [
    'name' => 'view.groups.page_user_groups',
    'parameters' => [
      'user' => \Drupal::currentUser()
        ->id(),
    ],
  ];
  \Drupal::moduleHandler()
    ->alter('social_group_overview_route', $route, $group);
  return $route;
}

/**
 * Form submit redirect for social groups.
 *
 * @param array $form
 *   Group add or group edit form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   Form state interface.
 */
function _social_group_edit_submit_redirect(array $form, FormStateInterface $form_state) {

  // Set redirect to the group about page.
  $route_parameters = $form_state
    ->getRedirect()
    ->getRouteParameters();
  if (!empty($route_parameters['group'])) {
    $form_state
      ->setRedirect('view.group_information.page_group_about', $route_parameters);
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function social_group_form_entity_access_by_field_visibility_form_alter(&$form, FormStateInterface $form_state, $form_id) {

  // Add option to disable public group creation.
  $form['#submit'][] = '_social_group_visibility_settings_submit';
}

/**
 * Form submit for visibility settings.
 *
 * @param array $form
 *   Group add or group edit form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   Form state interface.
 */
function _social_group_visibility_settings_submit(array $form, FormStateInterface $form_state) {

  /** @var \Drupal\user\Entity\Role $role */
  $role = Role::load('authenticated');
  $current_permission = $role
    ->hasPermission('create public_group group');
  $disable_public_visibility = $form_state
    ->getValue('disable_public_visibility');
  if ($disable_public_visibility === 1 && $current_permission === TRUE) {
    user_role_revoke_permissions($role
      ->id(), [
      'create public_group group',
    ]);
  }
  if ($disable_public_visibility === 0 && $current_permission === FALSE) {
    user_role_grant_permissions($role
      ->id(), [
      'create public_group group',
    ]);
  }
}

/**
 * Load group content label and group label.
 *
 * @return array
 *   array of group labels.
 */
function _social_group_get_group_labels() {

  // Load the entity label from the group content being handled.
  $group_content = \Drupal::routeMatch()
    ->getParameter('group_content')
    ->label();

  // Load group name.
  $group = \Drupal::routeMatch()
    ->getParameter('group')
    ->label();
  return [
    $group_content,
    $group,
  ];
}

/**
 * Prepares variables for profile templates.
 *
 * Default template: profile.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - elements: An array of elements to display in view mode.
 *   - profile: The profile object.
 *   - view_mode: View mode; e.g., 'full', 'teaser', etc.
 */
function social_group_preprocess_group(array &$variables) {

  /** @var \Drupal\social_group\GroupStatistics $group_statistics */
  $group_statistics = \Drupal::service('social_group.group_statistics');

  /** @var \Drupal\group\Entity\GroupInterface $group */
  $group = $variables['group'];
  $variables['title'] = $group
    ->label();
  $variables['joined'] = FALSE;
  $variables['closed_group'] = FALSE;
  $variables['cta'] = '';
  $variables['closed_group_lock'] = $variables['secret_group_shield'] = FALSE;
  $group_type_id = $group
    ->getGroupType()
    ->id();
  if (!array_key_exists('direct', _social_group_get_join_methods($group))) {
    $variables['closed_group_lock'] = TRUE;
  }
  $variables['group_type_id'] = $group_type_id;
  $variables['group_type'] = $group
    ->getGroupType()
    ->label();

  // From 10.0.0 onwards, we want to show group types differently.
  // It will be the value chosen while adding the group instead of saying
  // 'Flexible group'.
  if ($group_type_id === 'flexible_group' && $group
    ->hasField('field_group_type') && !empty($term = $group
    ->get('field_group_type')->entity) && \Drupal::config('social_group.settings')
    ->get('social_group_type_required')) {
    $variables['group_type'] = $term
      ->getName();
    $variables['group_type_icon'] = $term
      ->get('field_group_type_icon')->value;
  }

  // Render the group settings help, gear icon with popover.
  $group_settings_help = _social_group_render_group_settings_hero($group);
  $variables['group_settings_help'] = \Drupal::service('renderer')
    ->renderPlain($group_settings_help);
  $account = \Drupal::currentUser();

  // Set joined to true for teaser when current logged in
  // user is member of the group.
  if ($group
    ->getMember($account)) {
    $variables['joined'] = TRUE;
    if ($group
      ->hasPermission('leave group', $account)) {
      $variables['group_operations_url'] = Url::fromRoute('entity.group.leave', [
        'group' => $group
          ->id(),
      ]);
    }
  }
  elseif ($group
    ->hasPermission('join group', $account)) {

    // @todo switch this to get URL from routes correctly.
    $variables['group_operations_url'] = Url::fromRoute('entity.group.join', [
      'group' => $group
        ->id(),
    ]);
    if ($group_type_id === 'flexible_group') {
      $join_methods = $group
        ->get('field_group_allowed_join_method')
        ->getValue();
      $direct_option = in_array('direct', array_column($join_methods, 'value'), FALSE);
      if (!$direct_option) {
        $variables['group_operations_url'] = Url::fromRoute('entity.group.join', [
          'group' => $group
            ->id(),
        ]);
        $variables['closed_group'] = TRUE;
        $variables['cta'] = t('Invitation only');
      }
    }
  }
  elseif ($group_type_id == 'closed_group' && !$group
    ->hasPermission('manage all groups', $account)) {

    // Users can only be invited.
    $variables['group_operations_url'] = Url::fromRoute('entity.group.join', [
      'group' => $group
        ->id(),
    ]);
    $variables['closed_group'] = TRUE;
    $variables['cta'] = t('Invitation only');
  }

  // Add the hero styled image.
  if ($group
    ->hasField('field_group_image') && !empty($group
    ->get('field_group_image')->entity)) {

    // Fetch image style from field info.
    $original_image_style = $variables['content']['field_group_image'][0]['#image_style'] ?? '';
    $image_style = ImageStyle::load($original_image_style);
    if ($image_style instanceof ImageStyle) {
      $variables['group_hero_styled_image_url'] = $image_style
        ->buildUrl($group
        ->get('field_group_image')->entity
        ->getFileUri());

      // Check if this style is considered small.
      $overridden_image_style = Drupal::getContainer()
        ->get('social_group.hero_image')
        ->getGroupHeroImageStyle();
      if ($overridden_image_style !== $original_image_style) {
        $variables['group_hero_styled_image_url'] = ImageStyle::load($overridden_image_style)
          ->buildUrl($group
          ->get('field_group_image')->entity
          ->getFileUri());
      }
    }
  }

  // This should be determined regardless if there's an image or not.
  if (Drupal::getContainer()
    ->get('social_group.hero_image')
    ->isSmall()) {
    $variables['group_hero_small'] = TRUE;
  }

  // Add group edit url for management.
  if ($group instanceof Group) {

    // Get the current route name to check if
    // the user is on the edit or delete page.
    $route = \Drupal::routeMatch()
      ->getRouteName();
    if (!in_array($route, [
      'entity.group.edit_form',
      'entity.group.delete_form',
    ])) {
      if ($group
        ->access('update', $account)) {
        $variables['group_edit_url'] = $group
          ->toUrl('edit-form')
          ->toString();
        $variables['#cache']['contexts'][] = 'route.name';
      }
    }

    // Ensure all groups get the group.type differentiating labels in teasers
    // hero's and full nodes.
    // But also if the allowed join method is there, this should be added.
    $variables['#cache']['contexts'][] = 'group.type';
    if ($group
      ->hasField('field_group_allowed_join_method') && !empty($group
      ->getFieldValue('field_group_allowed_join_method', 'value'))) {
      $variables['#cache']['contexts'][] = 'social_group_join_method';
    }
  }

  // Count number of group members.
  $variables['group_members'] = $group_statistics
    ->getGroupMemberCount($group);

  // Prepare variables for statistic block.
  if ($variables['view_mode'] === 'statistic') {

    // Add context, since we render join / invite only etc links in the block.
    $variables['#cache']['contexts'][] = 'group';
    $variables['#cache']['contexts'][] = 'user';
    $about_url = Url::fromRoute('view.group_information.page_group_about', [
      'group' => $group
        ->id(),
    ]);
    $variables['about_url'] = $about_url;
    if ($group
      ->getGroupType()
      ->hasContentPlugin('group_node:event')) {
      $variables['group_events'] = $group_statistics
        ->getGroupNodeCount($group, 'event');
      $variables['group_events_label'] = \Drupal::translation()
        ->formatPlural($variables['group_events'], 'event', 'events');
    }
    if ($group
      ->getGroupType()
      ->hasContentPlugin('group_node:topic')) {
      $variables['group_topics'] = $group_statistics
        ->getGroupNodeCount($group, 'topic');
      $variables['group_topics_label'] = \Drupal::translation()
        ->formatPlural($variables['group_topics'], 'topic', 'topics');
    }
  }
}

/**
 * Implements hook_preprocess_HOOK().
 */
function social_group_preprocess_group__hero(array &$variables) {

  /** @var \Drupal\group\Entity\GroupInterface $group */
  $group = $variables['group'];
  $account = \Drupal::currentUser();
  if ($group
    ->bundle() == 'public_group' && $account
    ->isAnonymous()) {
    $variables['group_operations_url'] = Url::fromRoute('user.register', [], [
      'query' => [
        'destination' => Url::fromRoute('entity.group.join', [
          'group' => $group
            ->id(),
        ])
          ->toString(),
      ],
    ])
      ->toString();
  }

  // Render the group settings help, gear icon with popover.
  $group_settings_help = _social_group_render_group_settings_hero($group);
  $variables['group_settings_help'] = \Drupal::service('renderer')
    ->renderPlain($group_settings_help);
}

/**
 * Renders the group settings based on available fields for the hero.
 *
 * @param \Drupal\group\Entity\GroupInterface $group
 *   the Group interface.
 *
 * @return array
 *   the actual tooltip render array.
 */
function _social_group_render_group_settings_hero(GroupInterface $group) {
  $description = '';

  // Optional after 10.0.x we can render the group visibility.
  if ($group_visibility_option = _social_group_get_group_visibility($group)) {

    // Wrap our chosen description in a container.
    $description .= '<span class="title">' . t('Group visibility') . '</span>';
    $description .= '<div class="group-visibility-details container-background">';
    foreach ($group_visibility_option as $key => $group_visibility_string) {
      $description .= $group_visibility_string;
    }
    $description .= '</div>';
  }

  // Optional after 9.x we can render the allowed content visibility.
  if ($allowed_visibility_option = _social_group_get_allowed_visibility($group)) {

    // Wrap our chosen description in a container.
    $description .= '<span class="title">' . t('Group content visibility') . '</span>';
    $description .= '<div class="group-visibility-details container-background">';
    foreach ($allowed_visibility_option as $key => $allowed_visibility_string) {
      $description .= $allowed_visibility_string;
    }
    $description .= '</div>';
  }

  // Optional after 9.x we can render the join methods.
  if ($join_methods_option = _social_group_get_join_methods($group)) {

    // Wrap our chosen description in a container.
    $description .= '<span class="title">' . t('Join method') . '</span>';
    $description .= '<div class="group-visibility-details container-background">';
    foreach ($join_methods_option as $key => $join_methods_string) {
      $description .= $join_methods_string;
    }
    $description .= '</div>';
  }
  return social_group_render_tooltip('group_hero', t('Access permissions'), $description);
}

/**
 * Get the join methods of a group.
 *
 * @param \Drupal\group\Entity\GroupInterface $group
 *   the Group interface.
 *
 * @return array
 *   Returns join methods of a group.
 */
function _social_group_get_join_methods(GroupInterface $group) {
  $group_type = $group
    ->getGroupType();
  $group_type_id = $group_type
    ->id();
  $join_methods = [];

  // Get join method based on group type. TODO get it programmatically.
  switch ($group_type_id) {
    case 'secret_group':
    case 'closed_group':
      $join_methods['added'] = social_group_allowed_join_method_description('added');
      break;
    case 'public_group':
    case 'open_group':
      $join_methods['direct'] = social_group_allowed_join_method_description('direct');
      $join_methods['added'] = social_group_allowed_join_method_description('added');
      break;
    case 'flexible_group':

      // Try to retrieve join methods from Group directly.
      $allowed_options = $group
        ->get('field_group_allowed_join_method')
        ->getValue();
      foreach ($allowed_options as $option) {

        // Lets grab the value from the selected radio item.
        if (!empty($option['value']) && is_string($option['value'])) {
          $join_methods[$option['value']] = social_group_allowed_join_method_description($option['value']);
        }
      }
      break;
  }
  return $join_methods;
}

/**
 * Get the group visibility label of a group.
 *
 * @param \Drupal\group\Entity\GroupInterface $group
 *   the Group interface.
 * @param string $field_name
 *   The field name of the visibility field for a group type.
 *
 * @return array
 *   Returns the visibility options of a group.
 */
function _social_group_get_group_visibility(GroupInterface $group, $field_name = NULL) {
  $group_type = $group
    ->getGroupType();
  $group_type_id = $group_type
    ->id();
  $group_visibility = [];

  // Get join method based on group type.
  switch ($group_type_id) {
    case 'secret_group':
    case 'closed_group':
    case 'public_group':
    case 'open_group':
      return $group_visibility;
    case 'flexible_group':

      // By default we ship with the below field, lets grab its value(s).
      if ($group
        ->hasField('field_flexible_group_visibility')) {
        $visibility_values = $group
          ->get('field_flexible_group_visibility')
          ->getValue();

        // Lets grab the rendered description for the group visibility.
        if (!empty($visibility_values)) {
          foreach ($visibility_values as $visibility_value) {
            if (!empty($visibility_value['value']) && is_string($visibility_value['value'])) {
              $group_visibility[$visibility_value['value']] = social_group_group_visibility_description($visibility_value['value']);
            }
          }
        }
        return $group_visibility;
      }

      // If the above field doesn't exist the user might have a different one
      // so lets see if it's given as argument and get it's description.
      if ($field_name !== NULL && $group
        ->hasField($field_name)) {
        $visibility_values = $group
          ->get($field_name)
          ->getValue();

        // Lets grab the rendered description for the group visibility.
        if (!empty($visibility_values)) {
          foreach ($visibility_values as $visibility_value) {
            if (!empty($visibility_value['value']) && is_string($visibility_value['value'])) {
              $group_visibility[$visibility_value['value']] = social_group_group_visibility_description($visibility_value['value']);
            }
          }
        }
        return $group_visibility;
      }
      break;
    default:

      // For non Open Social group types, we can allow developers to pass
      // a field name to return a label instead.
      if ($field_name !== NULL && $group
        ->hasField($field_name)) {
        $group_visibility = $group
          ->get($field_name)
          ->getValue();
      }
      return $group_visibility;
  }
  return $group_visibility;
}

/**
 * Get the allowed visibility of a group.
 *
 * @param Drupal\group\Entity\GroupInterface $group
 *   the Group interface.
 *
 * @return array
 *   Returns allowed visibility of a group.
 */
function _social_group_get_allowed_visibility(GroupInterface $group) {
  $group_type = $group
    ->getGroupType();
  $group_type_id = $group_type
    ->id();
  $allowed_visibility = [];

  // Get allowed visibility based on group type. TODO get it programmatically.
  switch ($group_type_id) {
    case 'secret_group':
    case 'closed_group':
      $allowed_visibility['group'] = social_group_allowed_visibility_description('group');
      break;
    case 'open_group':
      $allowed_visibility['community'] = social_group_allowed_visibility_description('community');
      break;
    case 'public_group':
      $allowed_visibility['public'] = social_group_allowed_visibility_description('public');
      break;
    case 'flexible_group':

      // Try to retrieve allowed visibility from Group directly.
      $allowed_options = $group
        ->get('field_group_allowed_visibility')
        ->getValue();
      foreach ($allowed_options as $option) {
        if (!empty($option['value']) && is_string($option['value'])) {
          $allowed_visibility[$option['value']] = social_group_allowed_visibility_description($option['value']);
        }
      }
      break;
  }
  return $allowed_visibility;
}

/**
 * Returns a description array for the field_flexible_group_visibility options.
 *
 * @return string
 *   The render array containing the description.
 */
function social_group_group_visibility_description($key) {
  $description = '';

  // We need it to be specified otherwise we can't build the markup.
  if (empty($key)) {
    return $description;
  }

  // Add explanatory descriptive text after the icon.
  switch ($key) {
    case 'public':
      $description = '<p>';
      $description .= '<p><strong><svg class="icon-small"><use xlink:href="#icon-public"></use></svg></strong>';
      $description .= '<strong>' . t('Public')
        ->render() . '</strong>';
      $description .= ' - ' . t('All visitors of the platform can see this group')
        ->render();
      $description .= '</p>';
      break;
    case 'community':
      $description = '<p>';
      $description .= '<strong><svg class="icon-small"><use xlink:href="#icon-community"></use></svg></strong>';
      $description .= '<strong>' . t('Community')
        ->render() . '</strong>';
      $description .= ' - ' . t('Only members who are logged in can see this group')
        ->render();
      $description .= '</p>';
      break;
    case 'members':
      $description = '<p>';
      $description .= '<p><strong><svg class="icon-small"><use xlink:href="#icon-lock"></use></svg></strong>';
      $description .= '<strong>' . t('Group members only (Secret)')
        ->render() . '</strong>';
      $description .= ' - ' . t('Only group members can see this group')
        ->render();
      $description .= '</p>';
      break;
  }

  // Allow modules to provide their own markup for a given key in the
  // group_visibility #options array.
  \Drupal::moduleHandler()
    ->alter('social_group_group_visibility_description', $key, $description);
  return $description;
}

/**
 * Implements hook_entity_insert().
 */
function social_group_group_insert(GroupInterface $group) {

  // @todo Remove this when https://www.drupal.org/node/2702743 lands and make.
  // sure the settings will be implemented accordingly.
  if ($group
    ->getGroupType()
    ->id() === 'open_group' || $group
    ->getGroupType()
    ->id() === 'closed_group' || $group
    ->getGroupType()
    ->id() === 'flexible_group') {

    // Get the group owner.
    $account = $group
      ->getOwner();

    // Get membership.
    $content = $group
      ->getMember($account)
      ->getGroupContent();

    // Delete the initial created membership.
    $content
      ->delete();
    $grant_group_admin = FALSE;

    // If the user has this permission inside a group.
    if ($group
      ->hasPermission('manage all groups', $account)) {

      // Then we grant this user de Group Admin role.
      $grant_group_admin = TRUE;
    }

    // When a CM+ creates a group, it is given the group_manager role
    // alongside the group_admin role to keep the full control over the group.
    if ($grant_group_admin) {

      // Delete the initial created membership.
      $content
        ->delete();
      $plugin = $group
        ->getGroupType()
        ->getContentPlugin('group_membership');
      $values = [
        'group_roles' => [
          $group
            ->bundle() . '-group_admin',
          $group
            ->bundle() . '-group_manager',
        ],
      ];
      $group_content = GroupContent::create([
        'type' => $plugin
          ->getContentTypeConfigId(),
        'gid' => $group
          ->id(),
        'entity_id' => $group
          ->getOwnerId(),
      ] + $values);
      $group_content
        ->save();
    }
    else {

      // Create a new membership.
      $plugin = $group
        ->getGroupType()
        ->getContentPlugin('group_membership');
      $values = [
        'group_roles' => [
          $group
            ->bundle() . '-group_manager',
        ],
      ];
      $group_content = GroupContent::create([
        'type' => $plugin
          ->getContentTypeConfigId(),
        'gid' => $group
          ->id(),
        'entity_id' => $group
          ->getOwnerId(),
      ] + $values);
      $group_content
        ->save();
    }
  }
}

/**
 * Returns a description array for the field_group_allowed_join_method options.
 *
 * @param string $key
 *   The join method key.
 *
 * @return string
 *   The render array containing the description.
 */
function social_group_allowed_join_method_description($key) {
  $description = '';

  // We need it to be specified otherwise we can't build the markup.
  if (empty($key)) {
    return $description;
  }

  // Add explanatory descriptive text after the icon.
  switch ($key) {
    case 'added':
      $description = '<p><strong>' . t('Invite only')
        ->render() . '</strong>';
      $description .= ' - ' . t('users can only join this group if they are added/invited by group managers.')
        ->render();
      $description .= '</p>';
      break;
    case 'direct':
      $description = '<p><strong>' . t('Open to join')
        ->render() . '</strong>';
      $description .= ' - ' . t('users can join this group without approval.')
        ->render();
      $description .= '</p>';
      break;
    case 'request':
      $description = '<p><strong>' . t('Request to join')
        ->render() . '</strong>';
      $description .= ' - ' . t('users can "request to join" this group which group managers approve/decline.')
        ->render();
      $description .= '</p>';
      break;
  }

  // Allow modules to provide their own markup for a given key in the
  // join method #options array.
  \Drupal::moduleHandler()
    ->alter('social_group_allowed_join_method_description', $key, $description);
  return $description;
}

/**
 * Implements hook_entity_view_alter().
 */
function social_group_entity_view_alter(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display) {

  // For group entities.
  if ($entity instanceof Group) {

    // Add cache contexts for some view modes.
    switch ($build['#view_mode']) {
      case 'hero':

        // Add cache contexts for group type & permissions.
        // We need join / leave for the CTA and also update permissions
        // for the button to edit a group.
        $build['#cache']['contexts'][] = 'group.type';
        $build['#cache']['contexts'][] = 'user.group_permissions';
        $build['#cache']['tags'][] = 'group_block:' . $entity
          ->id();
        break;
      case 'teaser':
        $build['#cache']['contexts'][] = 'user';
        break;
    }
  }
}

/**
 * Implements hook_form_alter().
 */
function social_group_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  $social_group_types = [
    'open_group',
    'closed_group',
    'public_group',
  ];
  \Drupal::moduleHandler()
    ->alter('social_group_types', $social_group_types);
  $join_forms = [];
  $leave_forms = [];
  $membership_add_forms = [];
  $membership_edit_forms = [];
  $membership_delete_forms = [];
  $group_forms = [];
  foreach ($social_group_types as $social_group_type) {
    $join_forms[] = "group_content_{$social_group_type}-group_membership_group-join_form";
    $leave_forms[] = "group_content_{$social_group_type}-group_membership_group-leave_form";
    $membership_add_forms[] = "group_content_{$social_group_type}-group_membership_add_form";
    $membership_edit_forms[] = "group_content_{$social_group_type}-group_membership_edit_form";
    $membership_delete_forms[] = "group_content_{$social_group_type}-group_membership_delete_form";
    $group_forms['edit'][] = "group_{$social_group_type}_edit_form";
    $group_forms['add'][] = "group_{$social_group_type}_add_form";
    $group_forms['delete'][] = "group_{$social_group_type}_delete_form";
  }
  $action_forms = array_merge($join_forms, $leave_forms, $membership_delete_forms, $membership_add_forms);
  $membership_forms = array_merge($membership_add_forms, $membership_edit_forms);

  // Perform alterations on joining / leaving groups.
  if (in_array($form_id, $membership_delete_forms)) {
    $form['actions']['submit']['#submit'][] = '_social_membership_delete_form_submit';
    [
      $name,
      $group,
    ] = _social_group_get_group_labels();

    // Give a better title, description and submit button value.
    $form['#title'] = t('Remove %name from %group', [
      '%name' => $name,
      '%group' => $group,
    ]);
    $form['actions']['submit']['#value'] = t('Remove');
    $form['description']['#markup'] = t('Are you sure you want to remove %name from %group?', [
      '%name' => $name,
      '%group' => $group,
    ]);
  }

  // Set some helpful text on the group join form now it's there.
  if (in_array($form_id, $join_forms)) {
    $markup = t('By submitting this form you will become a member of the group. As a member of the group you will begin to receive notifications about changes and interactions within the group. Your profile will also be included in group membership.');

    // When the user is accepting an invite, change the text to something more
    // meaningful.
    if (\Drupal::routeMatch()
      ->getRouteName() === 'ginvite.invitation.accept') {
      $markup = t('Are you sure to accept the invitation and join this group? You can leave this group at any time.');
    }
    $form['help'] = [
      '#type' => 'item',
      '#markup' => $markup,
    ];
    $form['path']['#type'] = 'hidden';
  }

  // Perform alterations on joining / leaving groups.
  if (in_array($form_id, $action_forms)) {

    // Add cancel option on join and leave form.
    $form['actions']['cancel'] = [
      '#type' => 'submit',
      '#value' => t('Cancel'),
      '#submit' => [
        '_social_group_cancel_join_leave_form',
      ],
      '#limit_validation_errors' => [],
    ];
    $form['actions']['submit']['#submit'][] = '_social_group_action_form_submit';
  }
  if (in_array($form_id, $membership_forms) && Drupal::currentUser()
    ->id() != 1) {

    // Change titles on membership forms.
    $form['entity_id']['widget'][0]['target_id']['#title'] = t('Find people by name');
    $form['group_roles']['widget']['#title'] = t('Group roles');

    // Remove the 'group_admin' role in a generic way
    // for all (future) group types.
    foreach ($form['group_roles']['widget']['#options'] as $key => $value) {

      // Hide the submission for the Group Admin role.
      if (strpos($key, 'group_admin') != FALSE) {
        unset($form['group_roles']['widget']['#options'][$key]);
      }
    }
    $form['path']['#type'] = 'hidden';
  }

  // Change the form when adding members directly in groups.
  if (in_array($form_id, $membership_add_forms, TRUE) && \Drupal::routeMatch()
    ->getRouteName() !== 'grequest.group_request_membership_approve') {

    // Lets add the new select 2 widget to add members to a group.
    $form['entity_id']['widget'][0]['target_id'] = [
      '#title' => t('Find people by name or email address'),
      '#type' => 'select2',
      '#multiple' => TRUE,
      '#tags' => TRUE,
      '#autocomplete' => TRUE,
      '#select2' => [
        'placeholder' => t('Jane Doe'),
        'tokenSeparators' => [
          ',',
          ';',
        ],
      ],
      '#selection_handler' => 'social',
      '#target_type' => 'user',
      '#element_validate' => [
        '_social_group_unique_members',
      ],
    ];

    // Group roles and the automated URL alias don't make sense here.
    if (isset($form['group_roles'])) {
      unset($form['group_roles']);
    }
    if (isset($form['path'])) {
      unset($form['path']);
    }
  }

  // Check if form is group content create form.
  if (isset($form['#entity_type']) && $form['#entity_type'] === 'node') {
    $group = _social_group_get_current_group();
    if (!empty($group)) {

      // Add custom submit handler just for redirect purposes.
      // We don't want to override the form::save in group.
      $form['actions']['submit']['#submit'][] = '_social_group_node_form_submit';
    }
    else {

      // If node don't belong to any group as a group content plugin
      // we don't need group visibility field.
      if (method_exists($form_state
        ->getFormObject(), 'getEntity')) {

        /** @var \Drupal\Core\Entity\EntityInterface $node */
        $node = $form_state
          ->getFormObject()
          ->getEntity();

        /** @var \Drupal\group\Plugin\GroupContentEnablerManagerInterface $gc_manager */
        $gc_manager = \Drupal::service('plugin.manager.group_content_enabler');
        if (!$gc_manager
          ->getGroupContentTypeIds('group_node:' . $node
          ->bundle())) {
          unset($form['groups']);
        }
      }
    }
  }
  if (in_array($form_id, $group_forms['add']) || in_array($form_id, $group_forms['edit']) || in_array($form_id, $group_forms['delete'])) {

    // Add custom submit handler just for redirect purposes.
    $form['actions']['submit']['#submit'][] = '_social_group_edit_submit_redirect';
    if (in_array($form_id, $group_forms['add']) || in_array($form_id, $group_forms['edit'])) {
      $form['path']['#type'] = 'hidden';
      $form['actions']['submit']['#value'] = t('Save');

      // Hide default title from Address field.
      if (isset($form['field_group_address'])) {
        $form['field_group_address']['widget'][0]['#title'] = '';
      }
    }
    if (in_array($form_id, $group_forms['edit'])) {
      $social_group_form = SocialGroupAddForm::create(\Drupal::getContainer());
      $group_type_element = $social_group_form
        ->getGroupTypeElement(TRUE);

      // Get the current group.
      $group = _social_group_get_current_group();

      // Set the default value in the form.
      $group_type_element['widget']['#default_value'] = $group
        ->getGroupType()
        ->id();

      // If user doesn't have permission to change group types disable it.
      // Or if group types can't be edited due to visibility issues.
      if (!social_group_group_type_permission_check()) {
        $group_type_element['#disabled'] = TRUE;
      }
      else {
        $group_type_element['#prefix'] = '<div id="group-type-result"></div>';
        $group_type_element['widget']['#ajax'] = [
          'callback' => '_social_group_inform_group_type_selection',
          'effect' => 'fade',
          'event' => 'change',
        ];
      }
      $form['group_type'] = $group_type_element;

      // Disable all group types that can't be edited. Because they don't have
      // a visibility.
      foreach (array_keys($form['group_type']['widget']['#options']) as $type) {
        if (\Drupal::service('social_group.helper_service')
          ->getDefaultGroupVisibility($type) === NULL) {
          $form['group_type']['widget'][$type] = [
            '#disabled' => TRUE,
          ];
        }
      }
      $form['#fieldgroups']['group_settings']->children[] = 'group_type';
      $form['#group_children']['group_type'] = 'group_settings';
      $form['actions']['submit']['#submit'][] = '_social_group_type_edit_submit';
    }
    if (in_array($form_id, $group_forms['delete'])) {

      // Add custom submit handler to delete all content of the group.
      $group = _social_group_get_current_group();
      $form['description']['#markup'] = t('Are you sure you want to delete your group "@group" along with all of the posts, events and topics inside this group? This action cannot be undone.', [
        '@group' => $group
          ->label(),
      ]);
      $form['actions']['cancel'] = [
        '#type' => 'submit',
        '#value' => t('Cancel'),
        '#submit' => [
          '_social_group_cancel_join_leave_form',
        ],
        '#limit_validation_errors' => [],
      ];
      array_unshift($form['actions']['submit']['#submit'], '_social_group_delete_group');
    }
  }
  if (in_array($form_id, [
    'group_flexible_group_edit_form',
    'group_flexible_group_add_form',
  ])) {
    $join_method_default_value = 'added';

    // Ensure we have a better descriptive label.
    if (array_key_exists('added', $form['field_group_allowed_join_method']['widget']['#options'])) {
      $form['field_group_allowed_join_method']['widget']['#options']['added'] = t('Invite only');
    }
    if (array_key_exists('direct', $form['field_group_allowed_join_method']['widget']['#options'])) {
      $form['field_group_allowed_join_method']['widget']['#options']['direct'] = t('Open to join');
    }

    // If directly exists it's becoming the default.
    if (in_array('direct', $form['field_group_allowed_join_method']['widget']['#default_value'])) {
      $join_method_default_value = 'direct';
    }
    elseif (in_array('request', $form['field_group_allowed_join_method']['widget']['#default_value'])) {
      $join_method_default_value = 'request';
    }
    $form['field_group_allowed_join_method']['widget']['#type'] = 'radios';
    $form['field_group_allowed_join_method']['widget']['#default_value'] = $join_method_default_value;
  }

  // Exposed Filter block on the all-groups overview.
  if ($form['#id'] === 'views-exposed-form-newest-groups-page-all-groups' || $form['#id'] === 'views-exposed-form-search-groups-page-no-value' || $form['#id'] === 'views-exposed-form-search-groups-page') {
    $account = \Drupal::currentUser();
    if (!empty($form['type']['#options'])) {
      foreach ($form['type']['#options'] as $type => $label) {

        // All / Any we can skip they are optional translatable options
        // and not group types.
        if ($label instanceof TranslatableMarkup) {
          continue;
        }
        if (!social_group_can_view_groups_of_type($type, $account)) {
          unset($form['type']['#options'][$type]);
        }
      }
    }
  }
}

/**
 * Function to validate entries against group members.
 */
function _social_group_unique_members($element, &$form_state, $complete_form) {

  // Call the autocomplete function to make sure enrollees are unique.
  SocialGroupEntityAutocomplete::validateEntityAutocomplete($element, $form_state, $complete_form, TRUE);
}

/**
 * Function that gives information to a CM+ when they edit a Group Type.
 *
 * @return \Drupal\Core\Ajax\AjaxResponse
 *   An ajax command containing the informative text.
 */
function _social_group_inform_group_type_selection() {
  $text = t('Please note that changing the group type will also change the
  visibility of the group content and the way users can join the group.');
  $ajax_response = new AjaxResponse();
  $ajax_response
    ->addCommand(new HtmlCommand('#group-type-result', $text));
  return $ajax_response;
}

/**
 * Form submit for removing members from a group so we can clear caches.
 *
 * @throws \Drupal\Core\Entity\EntityStorageException
 */
function _social_group_type_edit_submit($form, FormStateInterface $form_state) {

  // Check if the group_type changed.
  // Visibility be empty for flexible groups.
  $group = _social_group_get_current_group();
  if (!empty($form['group_type']) && \Drupal::service('social_group.helper_service')
    ->getDefaultGroupVisibility($group
    ->getGroupType()
    ->id()) != NULL) {
    $default_type = $form['group_type']['widget']['#default_value'];
    $new_type = $form_state
      ->getValue('group_type')[0]['value'];

    // Update the Group entity and all it's content when the type changed.
    if ($new_type !== $default_type) {

      // Update the default visibility of all the content.
      GroupContentVisibilityUpdate::batchUpdateGroupContentVisibility($group, $new_type);
    }
  }
  if ($group instanceof GroupInterface) {

    // Make sure we clear cache tags accordingly.
    $cache_tags = _social_group_cache_tags($group);
    foreach ($cache_tags as $cache_tag) {
      Cache::invalidateTags([
        $cache_tag,
      ]);
    }
  }
}

/**
 * Form submit for removing members from a group so we can clear caches.
 */
function _social_membership_delete_form_submit($form, FormStateInterface $form_state) {
  $group = _social_group_get_current_group();
  if (is_object($group)) {

    // Invalidate cache tags.
    $cache_tags = _social_group_cache_tags($group);
    foreach ($cache_tags as $cache_tag) {
      Cache::invalidateTags([
        $cache_tag,
      ]);
    }
  }
}

/**
 * Form submit for group join / leave form.
 */
function _social_group_action_form_submit($form, FormStateInterface $form_state) {
  $group = _social_group_get_current_group();
  if (is_object($group)) {

    // Invalidate cache tags.
    $cache_tags = _social_group_cache_tags($group);
    foreach ($cache_tags as $cache_tag) {
      Cache::invalidateTags([
        $cache_tag,
      ]);
    }

    // Get form that was submitted.
    $complete_form = $form_state
      ->getCompleteForm();
    if (in_array($complete_form['#form_id'], [
      'group_content_' . $group
        ->bundle() . '-group_membership_group-join_form',
      'group_content_' . $group
        ->bundle() . '-group_membership_add_form',
    ])) {

      // Set redirect to group home page for people joining the group.
      $form_state
        ->setRedirect('entity.group.canonical', [
        'group' => $group
          ->id(),
        [],
      ]);

      // For adding people we redirect to the manage members page.
      if ($complete_form['#form_id'] == 'group_content_' . $group
        ->bundle() . '-group_membership_add_form') {

        // We passed the validation.
        // Let's create the Group Content for all our members just like
        // Group does it for creating a Group and adding its creator as member.
        $values = [
          'group_roles' => $form_state
            ->getValue('group_roles'),
        ];
        $count = 0;
        if (!empty($form_state
          ->getValue('entity_id_new'))) {

          // For multiple Group Members that are valid we add multiple entities.
          foreach ($form_state
            ->getValue('entity_id_new') as $key => $uid) {
            $group
              ->addMember(User::load($uid['target_id']), $values);
            $count++;
          }
        }

        // Add nice messages.
        if (!empty($count)) {
          $message = \Drupal::translation()
            ->formatPlural($count, '@count new member joined the group.', '@count new members joined the group.');
          \Drupal::messenger()
            ->addMessage($message, 'status');
        }
        $form_state
          ->setRedirect('view.group_manage_members.page_group_manage_members', [
          'group' => $group
            ->id(),
          [],
        ]);
      }
    }
    else {

      // Set redirect to the Group overview page
      // when a user saves their profile.
      $route = _social_group_get_overview_route($group);
      $form_state
        ->setRedirect($route['name'], $route['parameters']);
    }
  }
}

/**
 * This function checks if the user should get the admin role within a group.
 *
 * Also check if the user has already a membership.
 * If so, update it with the new role.
 */
function _social_group_grant_admin_role($uid, $gid) {
  $account = User::load($uid);
  $group = Group::load($gid);

  // Must be a valid account AND a valid group.
  if ($account instanceof User && $group instanceof Group) {

    // Must have manage all groups permission.
    // Otherwise normal flow will be fine.
    if (!$account
      ->hasPermission('manage all groups')) {
      return;
    }

    // Check if the user is already a member in the group
    // (could be in update mode here).

    /** @var \Drupal\group\GroupMembership $membership */
    $admin_role = $group
      ->bundle() . '-group_admin';
    $membership = $group
      ->getMember($account);

    // Check what roles are there.
    $roles = [];
    foreach ($membership
      ->getGroupContent()->group_roles as $group_role_ref) {
      $roles[] = $group_role_ref->target_id;
    }

    // No admin? Add it.
    if (!in_array($admin_role, $roles)) {
      $membership
        ->getGroupContent()->group_roles[] = [
        'target_id' => $admin_role,
      ];
      $membership
        ->getGroupContent()
        ->save();
    }
  }
}

/**
 * When creating a new group membership.
 *
 * @param \Drupal\group\Entity\GroupContentInterface $group_content
 *   The group content.
 */
function social_group_group_content_insert(GroupContentInterface $group_content) {
  if ($group_content
    ->getEntity()
    ->getEntityTypeId() == 'user') {
    if ($group_content
      ->bundle() == $group_content
      ->getGroup()
      ->bundle() . '-group_membership') {
      _social_group_grant_admin_role($group_content
        ->getEntity()
        ->id(), $group_content
        ->getGroup()
        ->id());
    }
  }
}

/**
 * When updating a group membership.
 *
 * @param \Drupal\group\Entity\GroupContentInterface $group_content
 *   The group content.
 */
function social_group_group_content_update(GroupContentInterface $group_content) {
  if ($group_content
    ->getEntity()
    ->getEntityTypeId() == 'user') {
    if ($group_content
      ->bundle() == $group_content
      ->getGroup()
      ->bundle() . '-group_membership') {
      _social_group_grant_admin_role($group_content
        ->getEntity()
        ->id(), $group_content
        ->getGroup()
        ->id());
    }
  }
}

/**
 * Form submit for membership edit form.
 *
 * @param array $form
 *   Membership edit form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   Form state interface.
 */
function _social_group_membership_edit_form_submit(array $form, FormStateInterface $form_state) {
  $group = _social_group_get_current_group();
  if (is_object($group)) {

    // Set redirect to the Group overview page when a user saves their profile.
    $form_state
      ->setRedirect('entity.group_content.collection', [
      'group' => $group
        ->id(),
      [],
    ]);
  }
}

/**
 * Form cancel for join and leave form.
 *
 * @param array $form
 *   Join and leave form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   Form state interface.
 */
function _social_group_cancel_join_leave_form(array $form, FormStateInterface $form_state) {

  // Get form that was submitted.
  $group = _social_group_get_current_group();
  if (is_object($group)) {

    // Set redirect only for cancel buttons. Just go back to the group page.
    $form_state
      ->setRedirect('entity.group.canonical', [
      'group' => $group
        ->id(),
      [],
    ]);
  }
}

/**
 * Form submit for group content create form.
 *
 * @param array $form
 *   Group node form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   Form state interface.
 */
function _social_group_node_form_submit(array $form, FormStateInterface $form_state) {
  $nid = $form_state
    ->getValue('nid');

  // Set redirect.
  $form_state
    ->setRedirect('entity.node.canonical', [
    'node' => $nid,
  ]);
}

/**
 * Alter the visibility field within groups.
 *
 * Implements hook_field_widget_form_alter().
 */
function social_group_field_widget_form_alter(&$element, FormStateInterface $form_state, $context) {

  /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
  $field_definition = $context['items']
    ->getFieldDefinition();

  // Unset the visibility field options which are not enabled.
  if ($field_definition
    ->getType() == 'entity_access_field') {
    $default_value = $context['items']
      ->getValue();
    if (isset($default_value[0]['value'])) {
      $element['#default_value'] = $default_value[0]['value'];
    }
    else {
      $default_visibility = \Drupal::configFactory()
        ->get('entity_access_by_field.settings')
        ->get('default_visibility');
      $element['#default_value'] = $default_visibility;
    }
    $entity = $form_state
      ->getFormObject()
      ->getEntity();
    $current_group = _social_group_get_current_group();
    if (!empty($current_group)) {
      $group_type = $current_group
        ->getGroupType();
      $group_type_id = $group_type
        ->id();

      // If it's an existing entity we dont need to set the default
      // rather follow the saved one.
      if (!$entity
        ->id()) {
        $element['#default_value'] = \Drupal::service('social_group.helper_service')
          ->getDefaultGroupVisibility($group_type_id);
      }
    }
    else {
      $group_type_id = NULL;

      // This is not a group so lets disable this visibility option.
      $element['group']['#disabled'] = TRUE;
    }
    $visibility_options = social_group_get_allowed_visibility_options_per_group_type($group_type_id, NULL, $entity, $current_group);

    // Based on the allowed visibility options for a group type,
    // we disable the options which are not valid for the group.
    // Remember for flexible groups we can have multiple options.
    foreach ($visibility_options as $visibility => $allowed) {
      $element[$visibility]['#disabled'] = !$allowed;

      // If the element is disabled, we also need to move it from
      // the default value.
      if (!$allowed && $element['#default_value'] === $visibility) {
        $element['#default_value'] = NULL;
      }
    }

    // This contains the group that is passed back from the form_state as
    // chosen by selecting a group in the $form['groups'] select widget.
    // With this we can ensure that on validation the content visibility
    // field is disabling the correct values based on the selected group.
    if (!empty($form_state
      ->getValue('groups'))) {
      $group_id = NULL;
      $group_value = $form_state
        ->getValue('groups');
      if (is_array($group_value)) {
        $group_id = $group_value[0]['target_id'];
      }
      if (is_string($group_value)) {
        $group_id = (int) $group_value;
      }

      // Load the correct group.
      $selected_groups = Group::loadMultiple([
        $group_id,
      ]);

      // Ensure we disable the correct visibility options.
      foreach ($selected_groups as $current_group) {
        if (!empty($current_group)) {
          $group_type = $current_group
            ->getGroupType();
          $group_type_id = $group_type
            ->id();
          $visibility_options = social_group_get_allowed_visibility_options_per_group_type($group_type_id, NULL, NULL, $current_group);
          foreach ($visibility_options as $visibility => $allowed) {
            $element[$visibility]['#disabled'] = !$allowed;

            // If the element is disabled, we also need to move it from
            // the default value.
            if (!$allowed && $element['#default_value'] === $visibility) {
              $element['#default_value'] = NULL;
            }
          }
        }
      }
    }

    // For flexible groups it could potentially still be empty.
    // But in any way, from the available options, if its empty
    // lets choose the most safe one.
    if ($element['#default_value'] === NULL) {

      // So if public isn't disabled, it will become public.
      if (!$element['public']['#disabled']) {
        $element['#default_value'] = 'public';
      }

      // So if community isn't disabled, it will become community.
      if (!$element['community']['#disabled']) {
        $element['#default_value'] = 'community';
      }

      // And if members isn't disabled, it will become members.
      if (!$element['group']['#disabled']) {
        $element['#default_value'] = 'group';
      }
    }
  }
}

/**
 * Implements hook_views_post_render().
 *
 * Alter "Group Members" views. Replace user IDs with profile teasers.
 */
function social_group_views_post_render(ViewExecutable $view, &$output, CachePluginBase $cache) {
  if ($view
    ->id() == 'group_members') {
    if (!empty($output['#rows'][0]['#rows'])) {
      foreach ($output['#rows'][0]['#rows'] as $key => $row) {

        // Get Group membership content entity.
        $group_content = $row['#group_content'];

        // Get Profile.
        $user_profile = _social_group_get_member_profile($group_content);
        if ($user_profile) {

          // Use teaser for page and small_teaser for block.
          $view_mode = $view->current_display === 'block_newest_members' ? 'small_teaser' : 'teaser';

          // Replace output with profile teaser.
          $output['#rows'][0]['#rows'][$key] = \Drupal::entityTypeManager()
            ->getViewBuilder('profile')
            ->view($user_profile, $view_mode);
        }
        else {

          // Remove output if user don't have profile (admin).
          unset($output['#rows'][0]['#rows'][$key]);
        }
      }
    }
  }
}

/**
 * Implements hook_menu_local_tasks_alter().
 */
function social_group_menu_local_tasks_alter(&$data, $route_name) {

  // Rename Group "Related Entities" tab.
  if (isset($data['tabs'][0]['group.content']['#link'])) {
    $data['tabs'][0]['group.content']['#link']['title'] = t('Manage members');
    $data['tabs'][0]['group.content']['#weight'] = 70;
  }

  // Remove the default View tab.
  if (isset($data['tabs'][0]['group.view'])) {
    unset($data['tabs'][0]['group.view']);
  }

  // Remove Edit tab. Edit will always go through Floating Edit Button.
  if (isset($data['tabs'][0]['group.edit_form'])) {
    unset($data['tabs'][0]['group.edit_form']);
  }
  $user = \Drupal::currentUser();

  // Get the Group object from the route.
  $group = Drupal::routeMatch()
    ->getParameter('group');
  if ($group instanceof GroupInterface) {

    /** @var \Drupal\group\Entity\GroupTypeInterface $group_type */
    $group_type = $group
      ->getGroupType()
      ->id();

    // Check if it's a closed group.
    if ($group_type === 'closed_group') {

      // And if the user is not user 1.
      if ($user
        ->id() !== 1) {
        if ($user
          ->hasPermission('manage all groups')) {
          return;
        }

        // If the user is not an member of this group.
        if (!$group
          ->getMember($user)) {

          // Disable these local tasks.
          $data['tabs'][0]['group.view'] = [];
          if (!$group
            ->hasPermission('administer members', $user)) {
            $data['tabs'][0]['group.content'] = [];
          }
          $data['tabs'][0]['social_group.events'] = [];
          $data['tabs'][0]['social_group.topics'] = [];
        }
      }
    }
  }
}

/**
 * Implements hook_local_tasks_alter().
 */
function social_group_local_tasks_alter(&$local_tasks) {

  // Remove local delete task from group page.
  unset($local_tasks['group.delete_form']);
  $local_tasks['group.content']['route_name'] = 'view.group_manage_members.page_group_manage_members';
  $local_tasks['group.content']['weight'] = 70;
}

/**
 * Implements hook_menu_local_actions_alter().
 */
function social_group_menu_local_actions_alter(&$local_actions) {

  // Remove "Add Relationship" button.
  if (isset($local_actions['group_content.add_page'])) {
    unset($local_actions['group_content.add_page']);
  }

  // Remove Create new entity in group.
  if (isset($local_actions['group_content.create_page'])) {
    unset($local_actions['group_content.create_page']);
  }
}

/**
 * Return user profile by given group membership content.
 *
 * @param \Drupal\group\Entity\GroupContent $group_content
 *   Group content entity.
 *
 * @return \Drupal\profile\Entity\Profile
 *   Returns the Profile entity for the member.
 */
function _social_group_get_member_profile(GroupContent $group_content) {
  $user_profile = NULL;

  // Get User entity.
  $user_entity = $group_content
    ->getEntity();
  if (!empty($user_entity)) {

    // Get Profile storage.
    $storage = \Drupal::entityTypeManager()
      ->getStorage('profile');
    if (!empty($storage)) {

      // Get Profile entity.
      $user_profile = $storage
        ->loadByUser($user_entity, 'profile');
    }
  }
  return $user_profile;
}

/**
 * Gets current Group entity from the route.
 *
 * @param \Drupal\node\NodeInterface|null $node
 *   (optional) The node object or NULL.
 *
 * @return \Drupal\group\Entity\GroupInterface|null
 *   Returns the group object.
 */
function _social_group_get_current_group($node = NULL) {
  $cache =& drupal_static(__FUNCTION__, []);

  // For the same $node input, within the same request the return is always
  // the same.
  $nid = NULL;
  if (is_null($node)) {
    $nid = -1;
  }
  elseif ($node instanceof NodeInterface) {
    $nid = $node
      ->id();
  }

  // If we have a cache key and it has a value, we're done early.
  if (!is_null($nid) && isset($cache[$nid])) {

    // Translate FALSE (so isset works) back to NULL.
    return $cache[$nid] ?: NULL;
  }
  $group = \Drupal::routeMatch()
    ->getParameter('group');
  if (!is_object($group) && !is_null($group)) {
    $group = \Drupal::entityTypeManager()
      ->getStorage('group')
      ->load($group);
  }
  else {
    $node = is_object($node) ? $node : \Drupal::routeMatch()
      ->getParameter('node');
    if (is_object($node)) {
      $node_entity = [
        'target_type' => 'node',
        'target_id' => $node
          ->id(),
      ];
      $gid_from_entity = \Drupal::service('social_group.helper_service')
        ->getGroupFromEntity($node_entity);
      if ($gid_from_entity !== NULL) {
        $group = \Drupal::entityTypeManager()
          ->getStorage('group')
          ->load($gid_from_entity);
      }
    }
  }

  // If we have a cache key we store the value.
  if (!is_null($nid)) {

    // Translate NULL to FALSE so that isset works.
    $cache[$nid] = $group ?? FALSE;
  }
  return $group;
}

/**
 * Get group cache tags.
 *
 * @param \Drupal\group\Entity\GroupInterface $group
 *   The GroupInterface.
 *
 * @return array
 *   An array of cache tags related to groups.
 */
function _social_group_cache_tags(GroupInterface $group) {

  // Group views.
  $tags = [
    'group_list',
    'group_content_list',
    'group_view',
    'group_content_view',
  ];

  // Add cache tags that are based on id.
  $tags[] = 'group_hero:' . $group
    ->id();

  // Add cache tags for the blocks.
  $tags[] = 'group_block:' . $group
    ->id();
  $current_user = \Drupal::currentUser();
  if ($group_membership = $group
    ->getMember($current_user)) {
    $group_content = $group_membership
      ->getGroupContent();
    $tags[] = 'group_content:' . $group_content
      ->id();
  }
  return $tags;
}

/**
 * Implements hook_block_view_BASE_BLOCK_ID_alter().
 *
 * Add Group cache context for system "Tabs" block.
 */
function social_group_block_view_local_tasks_block_alter(array &$build, BlockPluginInterface $block) {
  $build['#cache']['contexts'][] = 'user.group_permissions';
}

/**
 * Implements hook_block_build_alter().
 */
function social_group_block_build_alter(array &$build, BlockPluginInterface $block) {
  if (!empty($block
    ->getPluginId()) && ($block
    ->getPluginId() === 'views_exposed_filter_block:newest_groups-page_all_groups' || $block
    ->getPluginId() === 'views_exposed_filter_block:search_groups-page')) {

    // The Group Type filter has to listen to changes in
    // group type role permissions. Using the role list ensures
    // it's cleared correctly.
    $build['#cache']['tags'][] = 'config:group_role_list';
  }
}

/**
 * Implements hook_preprocess_HOOK().
 */
function social_group_preprocess_profile(&$variables) {
  $group = _social_group_get_current_group();
  if ($group instanceof Group && $variables['elements']['#view_mode'] == 'teaser') {
    $account = $variables['elements']['#profile']
      ->get('uid')->entity;
    if (!($member = $group
      ->getMember($account))) {
      return;
    }
    $roles = $member
      ->getRoles();
    if (isset($roles[$group
      ->bundle() . '-group_manager'])) {
      $variables['badges'] = [
        [
          'label' => t('Group manager'),
          'classes' => [
            'badge-secondary teaser__badge',
          ],
        ],
      ];
    }
  }
}

/**
 * Implements hook_preprocess_HOOK().
 */
function social_group_preprocess_views_view(&$variables) {

  /** @var \Drupal\views\ViewExecutable $view */
  $view =& $variables['view'];
  if ($view
    ->id() === 'group_manage_members') {
    $entity = \Drupal::entityTypeManager()
      ->getStorage('block')
      ->load('socialblue_local_actions');
    $variables['header']['actions'] = \Drupal::entityTypeManager()
      ->getViewBuilder('block')
      ->view($entity);
  }
}

/**
 * Implements hook_block_access().
 *
 * Check if the user is viewing a closed_group, check if the user is a member.
 * If the user is not a member, the user has no access to view this block.
 */
function social_group_block_access(Block $block, $operation, AccountInterface $account) {
  $block_id = $block
    ->getPluginId();

  // Lets hide the hero block when adding members directly, it doesnt
  // add anything there.
  if ($operation === 'view' && $block_id === 'group_hero_block') {
    $route_name = \Drupal::routeMatch()
      ->getRouteName();
    $excluded_routes = [
      'entity.group_content.add_form',
    ];
    if (in_array($route_name, $excluded_routes, TRUE)) {
      return AccessResult::forbidden();
    }
  }

  // This is a list of the blocks that this function cares about, if we're being
  // called for a different block we exit early.
  $managed_blocks = [
    'views_exposed_filter_block:newest_groups-page_all_groups',
    'views_block:groups-block_user_groups',
    'views_block:upcoming_events-upcoming_events_group',
    'views_block:latest_topics-group_topics_block',
  ];
  if (!in_array($block_id, $managed_blocks)) {
    return AccessResult::neutral();
  }
  $group = _social_group_get_current_group();
  $user = Drupal::currentUser();

  // Check if there is a group set and if its an closed group.
  if ($group && $group
    ->getGroupType()
    ->id() == 'closed_group' && $account
    ->id() != 1) {
    if ($account
      ->hasPermission('manage all groups')) {
      return AccessResult::neutral();
    }
    elseif (!$group
      ->getMember($user)) {

      // If it is closed and the current user is not an member of this group,
      // then it is not allowed to see these blocks.
      $forbidden_blocks = [
        'views_block:upcoming_events-upcoming_events_group',
        'views_block:latest_topics-group_topics_block',
      ];
      foreach ($forbidden_blocks as $forbidden_block) {
        if ($operation == 'view' && $block
          ->getPluginId() == $forbidden_block) {
          return AccessResult::forbidden();
        }
      }
    }
  }

  // Check if we're alloweed to view the joined groups block.
  if ($operation === 'view' && $block
    ->getPluginId() === 'views_block:groups-block_user_groups') {

    // Here we're going to assume by default access is not granted.
    $groupController = SocialGroupController::create(\Drupal::getContainer());
    $access = $groupController
      ->myGroupAccess($account);

    // If the 'myGroupAccess' returns 'AccessResultNeutral', we have to assume
    // that access must be denied.
    if ($access instanceof AccessResultNeutral) {

      // Return forbidden, since access was not explicitly granted.
      return AccessResult::forbidden();
    }
    return $access;
  }
  return AccessResult::neutral();
}

/**
 * Determine the amount of group_types a user can see.
 *
 * @param \Drupal\Core\Session\AccountInterface $account
 *   The user to check for.
 *
 * @return int
 *   The amount of group_types
 */
function _social_group_get_current_group_types(AccountInterface $account) {
  $group_types = 0;

  /** @var \Drupal\group\Entity\GroupTypeInterface $group_type */
  foreach (GroupType::loadMultiple() as $group_type) {
    $group_types += (int) social_group_can_view_groups_of_type($group_type
      ->id(), $account);
  }
  return $group_types;
}

/**
 * Delete the group and all of its content.
 */
function _social_group_delete_group() {

  // Get the group.
  if ($group = _social_group_get_current_group()) {
    $group_content_types = GroupContentType::loadByEntityTypeId('node');
    $group_content_types = array_keys($group_content_types);

    // Get all the node's related to the current group.
    $query = \Drupal::database()
      ->select('group_content_field_data', 'gcfd');
    $query
      ->addField('gcfd', 'entity_id');
    $query
      ->condition('gcfd.gid', $group
      ->id());
    $query
      ->condition('gcfd.type', $group_content_types, 'IN');
    $query
      ->execute()
      ->fetchAll();
    $entity_ids = $query
      ->execute()
      ->fetchAllAssoc('entity_id');

    // Store all the node ids.
    $nids = array_keys($entity_ids);

    // Get all the posts from this group.
    $query = \Drupal::database()
      ->select('post__field_recipient_group', 'pfrg');
    $query
      ->addField('pfrg', 'entity_id');
    $query
      ->condition('pfrg.field_recipient_group_target_id', $group
      ->id());
    $query
      ->execute()
      ->fetchAll();
    $post_ids = $query
      ->execute()
      ->fetchAllAssoc('entity_id');

    // Store all the post entity ids.
    $posts = array_keys($post_ids);

    // Pass the $nids and $posts as 2 parameters in the operations.
    // See /social_group/src/Controller/DeleteGroup.php for further process.
    $batch = [
      'title' => t('Deleting the group and all the content within the group...'),
      'init_message' => t("Preparing to delete the group and all it's topic's, event's and post's..."),
      'operations' => [
        [
          '\\Drupal\\social_group\\Controller\\DeleteGroup::deleteGroupAndContent',
          [
            $nids,
            $posts,
          ],
        ],
      ],
      'finished' => '\\Drupal\\social_group\\Controller\\DeleteGroup::deleteGroupAndContentFinishedCallback',
    ];
    batch_set($batch);
  }
}

/**
 * Get all open groups.
 */
function social_group_get_all_open_groups() {
  $query = Drupal::service('entity.query')
    ->get('group')
    ->condition('type', 'open_group');
  return $query
    ->execute();
}

/**
 * Get all open groups.
 */
function social_group_get_all_groups() {
  $query = Drupal::service('entity.query')
    ->get('group')
    ->sort('type');
  return $query
    ->execute();
}

/**
 * Get all group memberships for a certain user.
 *
 * @param int $uid
 *   The UID for which we fetch the groups it is member of.
 *
 * @return array
 *   List of group IDs the user is member of.
 *
 * @deprecated in social:8.x-4.2 and is removed from social:8.x-9.6.
 *   This function is moved to the service social_group.helper_service, use
 *   getAllGroupsForUser() instead.
 *
 * @see https://www.drupal.org/node/3026220
 */
function social_group_get_all_group_members($uid) {
  @trigger_error(__FUNCTION__ . ' is deprecated in social:8.x-4.2 and is removed from social:8.x-9.6. This function is moved to the service social_group.helper_service, use getAllGroupsForUser() instead. See https://www.drupal.org/node/3026220', E_USER_DEPRECATED);
  $groups =& drupal_static(__FUNCTION__);

  // Get the memberships for the user if they aren't known yet.
  if (!isset($groups[$uid])) {
    $group_content_types = GroupContentType::loadByEntityTypeId('user');
    $group_content_types = array_keys($group_content_types);
    $query = \Drupal::database()
      ->select('group_content_field_data', 'gcfd');
    $query
      ->addField('gcfd', 'gid');
    $query
      ->condition('gcfd.entity_id', $uid);
    $query
      ->condition('gcfd.type', $group_content_types, 'IN');
    $query
      ->execute()
      ->fetchAll();
    $group_ids = $query
      ->execute()
      ->fetchAllAssoc('gid');
    $groups[$uid] = array_keys($group_ids);
  }
  return $groups[$uid];
}

/**
 * Implements hook_field_info_alter().
 */
function social_group_field_info_alter(&$info) {
  if (isset($info['groups'])) {
    $info['groups']['default widget'] = 'social_group_selector_widget';
  }
}

/**
 * Implements hook_entity_base_field_info_alter().
 */
function social_group_entity_base_field_info_alter(&$fields, EntityTypeInterface $entity_type) {
  if ($entity_type
    ->id() === 'node' && isset($fields['groups'])) {

    /** @var \Drupal\Core\Field\FieldDefinitionInterface[] $fields */
    $fields['groups']
      ->setLabel(t('Group'))
      ->setDescription('')
      ->setSetting('handler', 'social')
      ->setDisplayOptions('form', [
      'type' => 'options_select',
      'settings' => [],
      'weight' => 16,
    ])
      ->setDisplayConfigurable('form', TRUE);
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * Make sure the correct group is saved when group is selected on node form.
 */
function social_group_form_node_form_alter(&$form, FormStateInterface $form_state) {
  if (isset($form['#entity_type']) && $form['#entity_type'] === 'node') {
    if (isset($form['groups'])) {

      // Check if we are adding a translation or it's the default being editted.

      /** @var \Drupal\Core\Entity\EntityForm $form_object */
      $form_object = $form_state
        ->getFormObject();

      /** @var \Drupal\social_node\Entity\Node $node */
      $node = $form_object
        ->getEntity();
      $is_original_language = (bool) $node
        ->getFieldValue('default_langcode', 'value');

      // If it's not the original, we can safely say its a translation being
      // added or editted. In that case we don't want users to override the
      // group or visibility. We only allow those changes on the default as
      // these can not change per language for now.
      if (!$is_original_language) {
        $form['field_content_visibility']['widget']['#disabled'] = TRUE;
        $form['field_content_visibility']['widget']['#description'] = t('Changing visibility is disabled when translating.');
        $form['groups']['widget']['#disabled'] = TRUE;
        $form['groups']['widget']['#description'] = t('Changing groups is disabled when translating.');
      }
      $change_fieldgroup_titles = [
        'group_topic_visibility',
        'group_event_visibility',
      ];
      foreach ($change_fieldgroup_titles as $fieldgroup_title) {
        if (isset($form['#fieldgroups'][$fieldgroup_title])) {
          $form['#fieldgroups'][$fieldgroup_title]->label = t('Access permissions')
            ->render();
        }
      }

      // Lets remove the original submit function in favor of this submit.
      foreach ($form['actions']['submit']['#submit'] as $submit_key => $submit_function) {
        if ($submit_function === 'group_content_entity_submit') {
          unset($form['actions']['submit']['#submit'][$submit_key]);
        }
      }

      /** @var \Drupal\node\NodeInterface $node */
      $node = $form_state
        ->getFormObject()
        ->getEntity();

      // Store if the node is new or not.
      $form['is_new'] = [
        '#type' => 'value',
        '#value' => $node
          ->isNew(),
      ];
      $form['actions']['submit']['#submit'][] = 'social_group_save_group_from_node';
    }
  }
}

/**
 * Form submit to save the group from a node form.
 *
 * @param array $form
 *   Node add or node edit form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   Form state interface.
 */
function social_group_save_group_from_node(array $form, FormStateInterface $form_state) {

  // Check if we are adding a translation or it's the default being editted.

  /** @var \Drupal\Core\Entity\EntityForm $form_object */
  $form_object = $form_state
    ->getFormObject();

  /** @var \Drupal\social_node\Entity\Node $node */
  $node = $form_object
    ->getEntity();
  $is_original_language = (bool) $node
    ->getFieldValue('default_langcode', 'value');

  // Check if the created node is new or updated.
  $is_new = NULL !== $form_state
    ->getValue('is_new') ? $form_state
    ->getValue('is_new') : FALSE;
  $original_groups = [];
  $groups_to_add = [];
  $groups_to_remove = [];
  foreach ($form_state
    ->getValue('groups') as $new_group_key => $new_group) {
    $groups_to_add[$new_group['target_id']] = $new_group['target_id'];
  }

  // The node already exist so lets also change the logic accordingly,
  // only if there is already a group that needs to be removed.
  if (!empty($form['groups']['widget']['#default_value']) && $form['#form_id'] === 'node_' . $node
    ->bundle() . '_edit_form') {
    $original_groups = $form['groups']['widget']['#default_value'];
    foreach ($original_groups as $original_group_key => $original_group) {
      if (!in_array($original_group, $groups_to_add)) {
        $groups_to_remove[$original_group] = $original_group;
      }
      else {
        unset($groups_to_add[$original_group]);
      }
    }
  }

  // Now make sure the relevant GroupContent is removed or added.
  // But only when we are on the original language, we don't want to update
  // this when translating. The field is disabled for that scenario so no
  // need to run this.
  if ($is_original_language) {
    $setGroupsForNodeService = \Drupal::service('social_group.set_groups_for_node_service');
    $setGroupsForNodeService
      ->setGroupsForNode($node, $groups_to_remove, $groups_to_add, $original_groups, $is_new);
  }
}

/**
 * Get the allowed visibility options for a given group type.
 *
 * @param string|null $group_type_id
 *   The group type. Can be NULL to get visibility when it is not in a group.
 * @param \Drupal\Core\Session\AccountInterface|null $account
 *   The account object that may have impact on the visibility options.
 *
 * @return array
 *   An array of visibility options for the given group type.
 */
function social_group_get_allowed_visibility_options_per_group_type($group_type_id, AccountInterface $account = NULL, $entity = NULL, $group = NULL) {

  // Get the logged in user.
  if ($account === NULL) {
    $account = \Drupal::currentUser();
  }
  $visibility_options = [];
  $visibility_options['public'] = FALSE;
  $visibility_options['community'] = TRUE;
  $visibility_options['group'] = FALSE;
  switch ($group_type_id) {
    case 'closed_group':
      $visibility_options['community'] = FALSE;
      $visibility_options['group'] = TRUE;
      break;
    case 'public_group':
      $visibility_options['public'] = TRUE;
      $visibility_options['community'] = FALSE;
      break;
    case 'open_group':
      $visibility_options['public'] = FALSE;
      $visibility_options['community'] = TRUE;
      $visibility_options['group'] = FALSE;
      break;
    case 'secret_group':
      $visibility_options['public'] = FALSE;
      $visibility_options['community'] = FALSE;
      $visibility_options['group'] = TRUE;
      break;
    case 'flexible_group':
      if (empty($group)) {
        $group = _social_group_get_current_group();
      }
      if ($group !== NULL) {
        $visibility_options['public'] = FALSE;
        $visibility_options['community'] = FALSE;
        $visibility_options['group'] = FALSE;

        // Try to retrieve allowed options from Group directly.
        $allowed_options = $group
          ->get('field_group_allowed_visibility')
          ->getValue();
        foreach ($allowed_options as $option) {
          $value = $option['value'];
          $visibility_options[$value] = TRUE;
        }
      }
      break;
    default:
      $config = \Drupal::config('entity_access_by_field.settings');
      $visibility_options['public'] = TRUE;
      if ($config
        ->get('disable_public_visibility') === 1 && !$account
        ->hasPermission('override disabled public visibility')) {

        // This is a new entity.
        if (!$entity) {
          $visibility_options['public'] = FALSE;
        }
        else {

          /** @var \Drupal\node\Entity\Node $entity */
          if ($entity
            ->hasField('field_content_visibility')) {
            $current_visibility = $entity
              ->get('field_content_visibility')
              ->getString();
            if ($current_visibility !== 'public') {
              $visibility_options['public'] = FALSE;
            }
          }
        }
      }
      $visibility_options['community'] = TRUE;
      $visibility_options['group'] = FALSE;
      break;
  }
  \Drupal::moduleHandler()
    ->alter('social_group_allowed_visibilities', $visibility_options, $group_type_id);
  return $visibility_options;
}

/**
 * Implements hook_views_query_alter().
 */
function social_group_views_query_alter(ViewExecutable $view, QueryPluginBase $query) {
  if (\Drupal::currentUser()
    ->isAuthenticated()) {
    return;
  }
  if (empty($view->rowPlugin) || !$view->rowPlugin instanceof EntityRow || $view->rowPlugin
    ->getEntityTypeId() != 'group') {
    return;
  }
  $found = FALSE;
  foreach ($query->where as &$conditions) {
    foreach ($conditions['conditions'] as &$condition) {
      if ($condition['field'] == 'groups_field_data.type') {
        $found = TRUE;
        break 2;
      }
    }
  }
  if (!$found) {
    unset($condition);
    $condition = [
      'field' => 'groups_field_data.type',
      'value' => [],
      'operator' => 'not in',
    ];
  }
  $hiddens = $condition['operator'] == 'not in';

  // Define the variable as array otherwise it can be interpreted as string
  // And breaking the code below.
  $condition['value'] = [];

  /** @var \Drupal\group\Entity\GroupTypeInterface $group_type */
  foreach (GroupType::loadMultiple() as $group_type) {
    $new = !in_array($group_type
      ->id(), $condition['value']);
    $permissions = $group_type
      ->getAnonymousRole()
      ->getPermissions();
    $hidden = !in_array('view group', $permissions);
    if ($new && $hiddens == $hidden) {
      $condition['value'][] = $group_type
        ->id();
    }
  }
  if (!$found) {
    if (isset($conditions)) {
      $conditions['conditions'][] = $condition;
    }
    else {
      $query->where[] = [
        'conditions' => [
          $condition,
        ],
        'type' => 'AND',
      ];
    }
  }
}

/**
 * Implements hook_user_cancel_methods_alter().
 */
function social_group_user_cancel_methods_alter(&$methods) {
  $methods['user_cancel_reassign']['title'] .= ' ' . t('Reassign your groups to the super administrator.');
}

/**
 * Implements hook_social_user_account_header_create_links().
 *
 * Adds the "Create Group" link to the content creation menu.
 * When the user can only create groups of one type, the user skips the
 * group type selection page.
 */
function social_group_social_user_account_header_create_links($context) {

  // Create url to create a group.
  $route_add_group = Url::fromRoute('entity.group.add_page');

  // If we have a user to work with, we can check if we should redirect it to
  // the create group overview or a single group.
  // If we do not have a user we'll default to the overview.
  if (!empty($context['user'])) {

    /** @var \Drupal\Core\Session\AccountInterface $user */
    $account = $context['user'];
    $route_add_group = \Drupal::service('social_group.helper_service')
      ->getGroupsToAddUrl($account) ?? $route_add_group;
  }
  return [
    'add_group' => [
      '#type' => 'link',
      '#attributes' => [
        'title' => new TranslatableMarkup('Create New Group'),
      ],
      '#title' => new TranslatableMarkup('New Group'),
      '#weight' => 500,
    ] + $route_add_group
      ->toRenderArray(),
  ];
}

/**
 * Implements hook_social_user_account_header_items().
 *
 * Adds the My Groups button to the account header block if it's enabled by a
 * site manager.
 */
function social_group_social_user_account_header_items(array $context) {
  if (\Drupal::config('social_user.navigation.settings')
    ->get('display_my_groups_icon') !== TRUE) {
    return [];
  }

  // We can't create a "My Groups" link without user.
  if (empty($context['user'])) {
    return [];
  }
  return [
    'groups' => [
      '#type' => 'account_header_element',
      '#wrapper_attributes' => [
        'class' => [
          'desktop',
        ],
      ],
      '#title' => new TranslatableMarkup('My Groups'),
      '#url' => Url::fromRoute('view.groups.page_user_groups', [
        'user' => $context['user']
          ->id(),
      ]),
      '#icon' => 'group',
      '#label' => new TranslatableMarkup('My Groups'),
    ],
  ];
}

/**
 * Implements hook_social_user_account_header_account_links().
 *
 * Adds the "View my groups" link to the user menu.
 */
function social_group_social_user_account_header_account_links(array $context) {

  // We require a user for this link.
  if (empty($context['user']) || !$context['user'] instanceof AccountInterface) {
    return [];
  }
  return [
    'my_groups' => [
      '#type' => 'link',
      '#attributes' => [
        'title' => new TranslatableMarkup('View my groups'),
      ],
      '#title' => new TranslatableMarkup('My groups'),
      '#weight' => 800,
    ] + Url::fromRoute('view.groups.page_user_groups', [
      'user' => $context['user']
        ->id(),
    ])
      ->toRenderArray(),
  ];
}

/**
 * Determine whether a user can see groups of a given type as an outsider.
 *
 * @param string $type
 *   The group type to check.
 * @param \Drupal\Core\Session\AccountInterface $account
 *   The user to check for.
 *
 * @return bool
 *   Whether the user is allowed to view groups of the given type.
 */
function social_group_can_view_groups_of_type(string $type, AccountInterface $account) : bool {
  $group_type = GroupType::load($type);

  // If the group type doesn't exist then the user can't see the group type.
  if ($group_type === FALSE || $group_type === NULL) {
    return FALSE;
  }

  // Anonymous users have a special role.
  if ($account
    ->isAnonymous()) {

    /** @var \Drupal\group\Entity\GroupRoleInterface $role */
    return $group_type
      ->getAnonymousRole()
      ->hasPermission('view group');
  }

  // The service that keeps platform roles in sync with group roles for
  // outsiders.

  /** @var \Drupal\group\GroupRoleSynchronizerInterface $group_role_synchroniser */
  $group_role_synchroniser = \Drupal::service('group_role.synchronizer');

  /** @var \Drupal\group\Entity\GroupRoleInterface[] $group_roles */
  $group_roles = $group_type
    ->getRoles();
  $user_roles = $account
    ->getRoles(TRUE);

  // Authenticated is a locked Role,
  // So first we check outsiders also easier on the performance :)
  $outsider = $group_type
    ->getOutsiderRole();
  if (NULL !== $outsider && $outsider
    ->hasPermission('view group')) {
    return TRUE;
  }

  // If members can see it, we also need to show group types because
  // we are not calculation memberships.
  $member = $group_type
    ->getMemberRole();
  if (NULL !== $member && $member
    ->hasPermission('view group')) {
    return TRUE;
  }

  // Check each role this user has whether its group counterpart has the correct
  // permission. This counts for the advanced group permissions.
  // Admin, SM, CM.
  foreach ($user_roles as $role) {
    $group_role = $group_role_synchroniser
      ->getGroupRoleId($type, $role);
    if ($group_roles[$group_role]
      ->hasPermission('view group')) {
      return TRUE;
    }
  }

  // The user has no role that can view the group type.
  return FALSE;
}

/**
 * Implements hook_entity_operation_alter().
 */
function social_group_entity_operation_alter(array &$operations, EntityInterface $entity) {
  if ($entity
    ->getEntityTypeId() === 'group_content') {
    $titles = [
      'edit' => t('Edit'),
      'delete' => t('Remove'),
      'view' => t('View'),
    ];
    foreach ($operations as $key => &$operation) {
      if (isset($titles[$key])) {
        $operation['title'] = $titles[$key];
      }
    }
  }
}

/**
 * Implements hook_default_route_group_tabs_alter().
 */
function social_group_social_group_default_route_tabs_alter(&$tabs) {

  // Unset some tabs created by group.
  unset($tabs['group.view'], $tabs['group.edit_form'], $tabs['group.content']);
}

/**
 * Implements hook_social_path_manager_group_tabs_alter().
 */
function social_group_social_path_manager_group_tabs_alter(array &$tabs) {

  // Unset some tabs created by group.
  unset($tabs['group.view'], $tabs['group.edit_form'], $tabs['group.content']);

  // Add manage members tab.
  $tabs['social_group.membership'] = 'view.group_manage_members.page_group_manage_members';
}

/**
 * Implements hook_batch_alter().
 */
function social_group_batch_alter(&$batch) {
  if (!isset($batch['source_url'])) {
    return;
  }
  $actions = [
    'social_group_members_export_member_action',
    'social_group_change_member_role_action',
    'social_group_delete_group_content_action',
    'social_group_send_email_action',
  ];

  /** @var \Drupal\Core\Url $url */
  $url =& $batch['source_url'];
  if ($url
    ->getRouteName() === 'social_group_gvbo.views_bulk_operations.confirm' || $url
    ->getRouteName() === 'views_bulk_operations.confirm' || $url
    ->getRouteName() === 'views_bulk_operations.execute_batch' || $url
    ->getRouteName() === 'social_group_gvbo.views_bulk_operations.execute_configurable') {

    // Get the action ID.
    $action_id = _social_group_get_action_id($batch);
    $batch['sets'][0]['results']['action'] = $action_id;
    if (in_array($action_id, $actions, TRUE)) {
      $batch['sets'][0]['finished'] = '_social_group_action_batch_finish';
    }
  }
}

/**
 * Function to get the action id of a batch.
 *
 * @param array $batch
 *   The batch array.
 *
 * @return string
 *   Returns the batch action id.
 */
function _social_group_get_action_id(array &$batch) {

  /** @var \Drupal\Core\Form\FormStateInterface $form_state */
  $form_state =& $batch['form_state'];
  $action_id = '';
  if ($form_state instanceof FormStateInterface) {
    $data = $form_state
      ->get('views_bulk_operations');
    $action_id = $data['action_id'];
  }
  else {
    foreach ($batch['sets'][0]['operations'] as $operations) {
      if (empty($operations) || !is_array($operations)) {
        break;
      }
      foreach ($operations as $operation) {
        if (empty($operation) || !is_array($operation)) {
          break;
        }
        foreach ($operation as $items) {
          if (empty($items) || !is_array($items)) {
            break;
          }
          if (!empty($items['action_id'])) {
            $action_id = $items['action_id'];
            break;
          }
        }
      }
    }
  }
  return $action_id;
}

/**
 * Action batch finished callback.
 *
 * @param bool $success
 *   Was the process successfull?
 * @param array $results
 *   Batch process results array.
 * @param array $operations
 *   Performed operations array.
 */
function _social_group_action_batch_finish($success, array $results, array $operations) {

  // When we do a bulk action on all the items in a view, across multiple pages,
  // the saveList function needs to be called. So after pre-populating the list
  // the actual action is performed on the entities.
  if (!empty($results['view_id']) && !empty($results['display_id'])) {
    ViewsBulkOperationsBatch::saveList(TRUE, $results, $operations);
    return;
  }
  $operations = array_count_values($results['operations']);
  $results_count = 0;
  foreach ($operations as $count) {
    $results_count += $count;
  }
  $hook = 'social_group_action_' . $results['action'] . '_finish';
  foreach (\Drupal::moduleHandler()
    ->getImplementations($hook) as $module) {
    $function = $module . '_' . $hook;
    $messages = $function($success);
    if (is_array($messages)) {
      $fields = 0;
      foreach ($messages as $type => $message) {
        if (($type === 'singular' || $type === 'plural') && !empty($message) && is_string($message)) {
          $fields++;
        }
      }
      if ($fields === 2) {
        $message = \Drupal::translation()
          ->formatPlural($results_count, $messages['singular'], $messages['plural']);
        $type = $success ? MessengerInterface::TYPE_STATUS : MessengerInterface::TYPE_WARNING;
        \Drupal::messenger()
          ->addMessage($message, $type);
      }
    }
  }
}

/**
 * Implements hook_social_event_action_ACTION_ID_finish().
 */
function social_group_social_group_action_social_group_members_export_member_action_finish($success) {
  if ($success) {
    return [
      'singular' => '1 selected member has been exported successfully',
      'plural' => '@count selected members have been exported successfully',
    ];
  }
  return [
    'singular' => '1 selected member has not been exported',
    'plural' => '@count selected members have not been exported',
  ];
}

/**
 * Implements hook_social_event_action_ACTION_ID_finish().
 */
function social_group_social_group_action_social_group_delete_group_content_action_finish($success) {
  if ($success) {
    return [
      'singular' => '1 selected member has been removed successfully',
      'plural' => '@count selected members have been removed successfully',
    ];
  }
  return [
    'singular' => '1 selected member has not been removed successfully',
    'plural' => '@count selected members have been removed successfully',
  ];
}

/**
 * Implements hook_social_event_action_ACTION_ID_finish().
 */
function social_group_social_group_action_social_group_change_member_role_action_finish($success) {
  if ($success) {
    return [
      'singular' => 'The role of 1 selected member has been changed successfully',
      'plural' => 'The role of @count selected members have been changed successfully',
    ];
  }
  return [
    'singular' => 'The role of 1 selected member has not been changed successfully',
    'plural' => 'The role of @count selected members have not been changed successfully',
  ];
}

/**
 * Implements hook_social_event_action_ACTION_ID_finish().
 */
function social_group_social_group_action_social_group_send_email_action_finish($success) {
  if ($success) {

    // When the queue storage module is enabled the email is send in the
    // background.
    if (\Drupal::moduleHandler()
      ->moduleExists('social_queue_storage')) {
      return [
        'singular' => 'Your email will be send to 1 selected member',
        'plural' => 'Your email will be send to @count selected members',
      ];
    }
    return [
      'singular' => 'Your email has been sent to 1 selected member successfully',
      'plural' => 'Your email has been sent to @count selected members successfully',
    ];
  }
  return [
    'singular' => 'Your email has not been sent to 1 selected member successfully',
    'plural' => 'Your email has not been sent to @count selected members successfully',
  ];
}

/**
 * Implements hook_preprocess_field().
 */
function social_group_preprocess_field(&$variables) {
  $formatter = $variables['element']['#formatter'];
  if (in_array($formatter, [
    'address_plain',
    'address_default',
  ])) {
    $entity = $variables['element']['#object'];
    if ($entity && $entity instanceof GroupInterface) {
      $social_group_settings = \Drupal::config('social_group.settings');
      $address_visibility_settings = $social_group_settings
        ->get('address_visibility_settings');
      if (isset($address_visibility_settings['street_code_private']) && !empty($address_visibility_settings['street_code_private'])) {
        if (!$entity
          ->getMember(\Drupal::currentUser())) {
          switch ($formatter) {
            case 'address_plain':
              if (isset($variables['items'][0]['content']['#address_line1'])) {
                $variables['items'][0]['content']['#address_line1'] = NULL;
              }
              if (isset($variables['items'][0]['content']['#postal_code'])) {
                $variables['items'][0]['content']['#postal_code'] = NULL;
              }
              break;
            case 'address_default':
              if (isset($variables['items'][0]['content']['address_line1']['#value'])) {
                $variables['items'][0]['content']['address_line1']['#value'] = '';
              }
              if (isset($variables['items'][0]['content']['postal_code']['#value'])) {
                $variables['items'][0]['content']['postal_code']['#value'] = '';
              }
              break;
          }
        }
      }
    }
  }
}

/**
 * Returns a new popover tooltip based on a descriptive text and field name.
 *
 * @param string $field_name
 *   The field name to create a unique id for.
 * @param \Drupal\Core\StringTranslation\TranslatableMarkup $data_title
 *   The title of the pop-up.
 * @param string $description
 *   A string containing the description, this needs to be rendered markup.
 *
 * @return array
 *   The render array containing the tooltip.
 */
function social_group_render_tooltip($field_name, TranslatableMarkup $data_title, $description) {
  $build = [];
  $id = Html::getUniqueId('tooltip-' . $field_name);
  $icon = Bootstrap::glyphicon('question-sign');
  $title = '';

  // We special case this for the hero.
  if ($field_name === 'group_hero') {
    $data_title = t('Access permissions');

    // For SKY we can render a bigger icon.
    $icon_size = 'icon-small';
    if (theme_get_setting('style') === 'sky') {
      $icon_size = 'icon-medium';
      $title = t('Access permissions');
    }
    $icon = [
      '#type' => "inline_template",
      '#template' => '<svg class="icon-white ' . $icon_size . '"><use xlink:href="#icon-cog"></use></svg>',
    ];
  }
  $build['toggle'] = [
    '#type' => 'link',
    '#url' => Url::fromUserInput("#{$id}"),
    '#icon' => $icon,
    '#title' => $title,
    '#attributes' => [
      'class' => [
        'icon-before',
      ],
      'data-toggle' => 'popover',
      'data-container' => 'body',
      'data-html' => 'true',
      'data-placement' => 'bottom',
      'data-title' => $data_title ?: '',
    ],
  ];
  $build['requirements'] = [
    '#type' => 'container',
    '#theme_wrappers' => [
      'container__file_upload_help',
    ],
    '#attributes' => [
      'id' => $id,
      'class' => [
        'hidden',
        'help-block',
      ],
      'aria-hidden' => 'true',
    ],
  ];

  // As documented in Render API, Note that the value is passed through
  // \Drupal\Component\Utility\Xss::filterAdmin(), which strips known XSS
  // vectors while allowing a permissive list of HTML tags.
  $build['requirements']['descriptions'] = [
    '#markup' => $description,
    '#allowed_tags' => [
      'strong',
      'span',
      'svg',
      'p',
      'div',
      'em',
      'img',
      'a',
      'span',
      'use',
    ],
  ];
  $variables['popover'] = $build;
  return $build;
}

/**
 * Implements template_preprocess_form_element().
 */
function social_group_preprocess_fieldset(&$variables) {

  // Make sure our flexible group visibility field renders a tooltip, since
  // this field is rendered as fieldset with legend and radios as children
  // we need to do it in this preprocess.
  $element = $variables['element'];
  if (!empty($element['#field_name']) && $element['#field_name'] === 'field_content_visibility') {
    $description = '';
    foreach ($element['#options'] as $key => $label) {
      $description .= social_group_allowed_visibility_description($key);
    }

    // Render a specific tooltip based on a field name and description.
    // This is done in the fieldset, next to the <legend>.
    $variables['popover'] = social_group_render_tooltip('field_content_visibility', t('Visibility'), $description);
  }
}

/**
 * Returns a description array for the field_flexible_group_visibility options.
 *
 * @param string $key
 *   The join method key.
 *
 * @return string
 *   The render array containing the description.
 */
function social_group_allowed_visibility_description($key) {
  $description = '';

  // We need it to be specified otherwise we can't build the markup.
  if (empty($key)) {
    return $description;
  }

  // Add explanatory descriptive text after the icon.
  switch ($key) {
    case 'public':

      // If we want to have the title, lets start with it, otherwise open our
      // paragraph.
      $description = '<p>';
      $description .= '<strong><svg class="icon-small"><use xlink:href="#icon-public"></use></svg></strong>';
      $description .= '<strong>' . t('Public')
        ->render() . '</strong>';
      $description .= ' - ' . t('visible to all visitors to the platform.')
        ->render();
      $description .= '</p>';
      break;
    case 'community':

      // If we want to have the title, lets start with it, otherwise open our
      // paragraph.
      $description = '<p>';
      $description .= '<strong><svg class="icon-small"><use xlink:href="#icon-community"></use></svg></strong>';
      $description .= '<strong>' . t('Community')
        ->render() . '</strong>';
      $description .= ' - ' . t('only visible to all logged-in users.')
        ->render();
      $description .= '</p>';
      break;
    case 'group':

      // If we want to have the title, lets start with it, otherwise open our
      // paragraph.
      $description = '<p>';
      $description .= '<strong><svg class="icon-small"><use xlink:href="#icon-lock"></use></svg></strong>';
      $description .= '<strong>' . t('Group members only')
        ->render() . '</strong>';
      $description .= ' - ' . t('only visible to logged-in group members.')
        ->render();
      $description .= '</p>';
      break;
  }

  // Allow modules to provide their own markup for a given key in the
  // group_visibility #options array.
  \Drupal::moduleHandler()
    ->alter('social_group_content_visibility_description', $key, $description);
  return $description;
}

Functions

Namesort descending Description
social_group_allowed_join_method_description Returns a description array for the field_group_allowed_join_method options.
social_group_allowed_visibility_description Returns a description array for the field_flexible_group_visibility options.
social_group_batch_alter Implements hook_batch_alter().
social_group_block_access Implements hook_block_access().
social_group_block_build_alter Implements hook_block_build_alter().
social_group_block_view_local_tasks_block_alter Implements hook_block_view_BASE_BLOCK_ID_alter().
social_group_can_view_groups_of_type Determine whether a user can see groups of a given type as an outsider.
social_group_entity_base_field_info_alter Implements hook_entity_base_field_info_alter().
social_group_entity_operation_alter Implements hook_entity_operation_alter().
social_group_entity_type_alter Implements hook_entity_type_alter().
social_group_entity_view_alter Implements hook_entity_view_alter().
social_group_field_info_alter Implements hook_field_info_alter().
social_group_field_widget_form_alter Alter the visibility field within groups.
social_group_form_alter Implements hook_form_alter().
social_group_form_entity_access_by_field_visibility_form_alter Implements hook_form_FORM_ID_alter().
social_group_form_node_form_alter Implements hook_form_FORM_ID_alter().
social_group_get_allowed_visibility_options_per_group_type Get the allowed visibility options for a given group type.
social_group_get_all_groups Get all open groups.
social_group_get_all_group_members Deprecated Get all group memberships for a certain user.
social_group_get_all_open_groups Get all open groups.
social_group_group_content_insert When creating a new group membership.
social_group_group_content_update When updating a group membership.
social_group_group_insert Implements hook_entity_insert().
social_group_group_type_permission_check Check if a User is able to edit a Group's GroupType.
social_group_group_visibility_description Returns a description array for the field_flexible_group_visibility options.
social_group_local_tasks_alter Implements hook_local_tasks_alter().
social_group_menu_local_actions_alter Implements hook_menu_local_actions_alter().
social_group_menu_local_tasks_alter Implements hook_menu_local_tasks_alter().
social_group_preprocess_field Implements hook_preprocess_field().
social_group_preprocess_fieldset Implements template_preprocess_form_element().
social_group_preprocess_group Prepares variables for profile templates.
social_group_preprocess_group__hero Implements hook_preprocess_HOOK().
social_group_preprocess_profile Implements hook_preprocess_HOOK().
social_group_preprocess_views_view Implements hook_preprocess_HOOK().
social_group_render_tooltip Returns a new popover tooltip based on a descriptive text and field name.
social_group_save_group_from_node Form submit to save the group from a node form.
social_group_social_group_action_social_group_change_member_role_action_finish Implements hook_social_event_action_ACTION_ID_finish().
social_group_social_group_action_social_group_delete_group_content_action_finish Implements hook_social_event_action_ACTION_ID_finish().
social_group_social_group_action_social_group_members_export_member_action_finish Implements hook_social_event_action_ACTION_ID_finish().
social_group_social_group_action_social_group_send_email_action_finish Implements hook_social_event_action_ACTION_ID_finish().
social_group_social_group_default_route_tabs_alter Implements hook_default_route_group_tabs_alter().
social_group_social_path_manager_group_tabs_alter Implements hook_social_path_manager_group_tabs_alter().
social_group_social_user_account_header_account_links Implements hook_social_user_account_header_account_links().
social_group_social_user_account_header_create_links Implements hook_social_user_account_header_create_links().
social_group_social_user_account_header_items Implements hook_social_user_account_header_items().
social_group_theme Implements hook_theme().
social_group_user_cancel_methods_alter Implements hook_user_cancel_methods_alter().
social_group_views_post_render Implements hook_views_post_render().
social_group_views_query_alter Implements hook_views_query_alter().
template_preprocess_group_settings_help Prepares variables for group settings help text templates.
_social_group_action_batch_finish Action batch finished callback.
_social_group_action_form_submit Form submit for group join / leave form.
_social_group_cache_tags Get group cache tags.
_social_group_cancel_join_leave_form Form cancel for join and leave form.
_social_group_delete_group Delete the group and all of its content.
_social_group_edit_submit_redirect Form submit redirect for social groups.
_social_group_get_action_id Function to get the action id of a batch.
_social_group_get_allowed_visibility Get the allowed visibility of a group.
_social_group_get_current_group Gets current Group entity from the route.
_social_group_get_current_group_types Determine the amount of group_types a user can see.
_social_group_get_group_labels Load group content label and group label.
_social_group_get_group_visibility Get the group visibility label of a group.
_social_group_get_join_methods Get the join methods of a group.
_social_group_get_member_profile Return user profile by given group membership content.
_social_group_get_overview_route Get group overview route.
_social_group_grant_admin_role This function checks if the user should get the admin role within a group.
_social_group_inform_group_type_selection Function that gives information to a CM+ when they edit a Group Type.
_social_group_membership_edit_form_submit Form submit for membership edit form.
_social_group_node_form_submit Form submit for group content create form.
_social_group_render_group_settings_hero Renders the group settings based on available fields for the hero.
_social_group_type_edit_submit Form submit for removing members from a group so we can clear caches.
_social_group_unique_members Function to validate entries against group members.
_social_group_visibility_settings_submit Form submit for visibility settings.
_social_membership_delete_form_submit Form submit for removing members from a group so we can clear caches.