Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 122
0.00% covered (danger)
0.00%
0 / 58
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
Collaboration
0.00% covered (danger)
0.00%
0 / 117
0.00% covered (danger)
0.00%
0 / 58
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 15
1260
0.00% covered (danger)
0.00%
0 / 1
 create
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 defaultConfiguration
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildConfigurationForm
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 configurationSummary
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 build
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
30
 alterRenderable
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onAttachToRoot
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onAttachToSlot
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onMove
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onHistoryChange
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onUpdate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onDelete
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 removeInactiveUsers
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 buildRenderable
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
90
 rebuild
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3declare(strict_types=1);
4
5namespace Drupal\display_builder\Plugin\display_builder\Island;
6
7use Drupal\Core\Datetime\DateFormatterInterface;
8use Drupal\Core\Entity\EntityFieldManagerInterface;
9use Drupal\Core\Form\FormStateInterface;
10use Drupal\Core\Session\AccountInterface;
11use Drupal\Core\StringTranslation\TranslatableMarkup;
12use Drupal\Core\Url;
13use Drupal\display_builder\Attribute\Island;
14use Drupal\display_builder\InstanceInterface;
15use Drupal\display_builder\IslandConfigurationFormInterface;
16use Drupal\display_builder\IslandConfigurationFormTrait;
17use Drupal\display_builder\IslandPluginBase;
18use Drupal\display_builder\IslandType;
19use Drupal\file\FileInterface;
20use Symfony\Component\DependencyInjection\ContainerInterface;
21
22/**
23 * Real-time collaboration island plugin implementation.
24 */
25#[Island(
26  id: 'collaboration',
27  label: new TranslatableMarkup('Real-time collaboration'),
28  description: new TranslatableMarkup('Allow concurrent editing with multiple users.'),
29  type: IslandType::Button,
30)]
31class Collaboration extends IslandPluginBase implements IslandConfigurationFormInterface {
32
33  use IslandConfigurationFormTrait;
34
35  /**
36   * Seconds in 15 minutes.
37   */
38  private const SECONDS_IN_15_MINUTES = 900;
39
40  /**
41   * The current user.
42   */
43  protected AccountInterface $currentUser;
44
45  /**
46   * The date formatter.
47   */
48  protected DateFormatterInterface $dateFormatter;
49
50  /**
51   * The entity field manager.
52   */
53  protected EntityFieldManagerInterface $entityFieldManager;
54
55  /**
56   * {@inheritdoc}
57   */
58  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
59    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
60    $instance->currentUser = $container->get('current_user');
61    $instance->dateFormatter = $container->get('date.formatter');
62    $instance->entityFieldManager = $container->get('entity_field.manager');
63
64    return $instance;
65  }
66
67  /**
68   * {@inheritdoc}
69   */
70  public function defaultConfiguration(): array {
71    return [
72      'image_field' => 'user_picture',
73      'image_style' => 'thumbnail',
74    ];
75  }
76
77  /**
78   * {@inheritdoc}
79   */
80  public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
81    $options = [
82      '' => $this->t('- None -'),
83    ];
84    $configuration = $this->getConfiguration();
85
86    $fields = $this->entityFieldManager->getFieldDefinitions('user', 'user');
87
88    foreach ($fields as $field_id => $field) {
89      if ($field->getType() === 'image') {
90        $options[$field_id] = $field->getLabel();
91      }
92    }
93    $form['image_field'] = [
94      '#title' => $this->t('Image field'),
95      '#type' => 'select',
96      '#default_value' => $configuration['image_field'],
97      '#options' => $options,
98    ];
99
100    $styles = $this->entityTypeManager->getStorage('image_style')->loadMultiple();
101    $options = [
102      '' => $this->t('- None -'),
103    ];
104
105    foreach ($styles as $name => $style) {
106      $options[$name] = $style->label();
107    }
108    $form['image_style'] = [
109      '#title' => $this->t('Image style'),
110      '#type' => 'select',
111      '#default_value' => $configuration['image_style'],
112      '#options' => $options,
113    ];
114
115    return $form;
116  }
117
118  /**
119   * {@inheritdoc}
120   */
121  public function configurationSummary(): array {
122    $conf = $this->getConfiguration();
123    $with_picture_text = $conf['image_style'] ? $this->t('With @style picture.', ['@style' => $conf['image_style']]) : $this->t('With picture');
124
125    return [
126      $conf['image_field'] ? $with_picture_text : $this->t('Without picture.'),
127    ];
128  }
129
130  /**
131   * {@inheritdoc}
132   */
133  public function build(InstanceInterface $builder, array $data = [], array $options = []): array {
134    $builder_id = (string) $builder->id();
135    // @todo pass \Drupal\display_builder\InstanceInterface object in
136    // parameters instead of loading again.
137    /** @var \Drupal\display_builder\InstanceInterface $builder */
138    $builder = $this->entityTypeManager->getStorage('display_builder_instance')->load($builder_id);
139    $this->builder = $builder;
140    $users = $builder->getUsers();
141    $current_user = $this->currentUser->id();
142    $users = $this->removeInactiveUsers($users);
143
144    // If no users (this situation must not happen), don't show anything.
145    if (\count($users) === 0) {
146      return [];
147    }
148
149    if (\array_key_exists($current_user, $users)) {
150      // If the only user is the current user, don't show anything.
151      if (\count($users) === 1) {
152        return [];
153      }
154      // Move current user at the beginning of the list.
155      $users = [$current_user => $users[$current_user]] + $users;
156
157      return $this->buildRenderable($users);
158    }
159
160    // If the only user is not the current user, add they at the start.
161    if (\count($users) === 1) {
162      $users = [$current_user => NULL] + $users;
163    }
164
165    return $this->buildRenderable($users);
166  }
167
168  /**
169   * {@inheritdoc}
170   */
171  public function alterRenderable(InstanceInterface $instance, array $build): array {
172    $build['#attributes'] = [
173      'hx-ext' => 'sse',
174      'sse-connect' => Url::fromRoute('display_builder.api_sse', ['display_builder_instance' => (string) $instance->id()])->toString(),
175    ];
176    // We don't attach it from the ::build() because the island doesn't
177    // always render something in the toolbar.
178    $build['#attached']['library'][] = 'display_builder/htmx_sse';
179
180    return $build;
181  }
182
183  /**
184   * {@inheritdoc}
185   */
186  public function onAttachToRoot(string $builder_id, string $instance_id): array {
187    return $this->rebuild($builder_id);
188  }
189
190  /**
191   * {@inheritdoc}
192   */
193  public function onAttachToSlot(string $builder_id, string $instance_id, string $parent_id): array {
194    return $this->rebuild($builder_id);
195  }
196
197  /**
198   * {@inheritdoc}
199   */
200  public function onMove(string $builder_id, string $instance_id): array {
201    return $this->rebuild($builder_id);
202  }
203
204  /**
205   * {@inheritdoc}
206   */
207  public function onHistoryChange(string $builder_id): array {
208    return $this->rebuild($builder_id);
209  }
210
211  /**
212   * {@inheritdoc}
213   */
214  public function onUpdate(string $builder_id, string $instance_id): array {
215    return $this->rebuild($builder_id);
216  }
217
218  /**
219   * {@inheritdoc}
220   */
221  public function onDelete(string $builder_id, string $parent_id): array {
222    return $this->rebuild($builder_id);
223  }
224
225  /**
226   * Remove inactive users.
227   *
228   * @param array $users
229   *   Each key is an User entity ID, each value is a timestamp.
230   *
231   * @return array
232   *   Each key is an User entity ID, each value is a timestamp.
233   */
234  protected function removeInactiveUsers(array $users): array {
235    foreach ($users as $user_id => $time) {
236      if (\time() - $time > self::SECONDS_IN_15_MINUTES) {
237        unset($users[$user_id]);
238      }
239    }
240
241    return $users;
242  }
243
244  /**
245   * Build renderable.
246   *
247   * @param array $users
248   *   Each key is an User entity ID, each value is a timestamp.
249   *
250   * @return array
251   *   A renderable array.
252   */
253  protected function buildRenderable(array $users): array {
254    $avatars = [];
255    $configuration = $this->getConfiguration();
256
257    foreach ($users as $user_id => $time) {
258      /** @var \Drupal\user\UserInterface $user */
259      $user = $this->entityTypeManager->getStorage('user')->load($user_id);
260
261      if (!$user) {
262        // For example, if the user was deleted.
263        continue;
264      }
265      $avatar = [
266        '#type' => 'component',
267        '#component' => 'display_builder:avatar',
268        '#props' => [
269          'name' => $user->getDisplayName(),
270        ],
271        '#attributes' => [
272          'style' => '--size: 38px',
273        ],
274      ];
275
276      if ($time) {
277        // We can't use DateFormatterInterface::formatTimeDiffSince() because
278        // the displayed value will become obsolete if the island is not updated
279        // for a while.
280        $time = $this->dateFormatter->format($time, 'custom', 'G:i');
281        $avatar['#props']['name'] .= ', ' . $this->t('at @time', ['@time' => $time]);
282      }
283
284      if (isset($configuration['image_field']) && $user->hasField($configuration['image_field'])) {
285        $image = $user->get($configuration['image_field'])->entity;
286
287        if ($image instanceof FileInterface) {
288          $image_style = $configuration['image_style'];
289          $style = $this->entityTypeManager->getStorage('image_style')->load($image_style);
290          $avatar['#props']['image'] = $style ? $style->buildUri($image->getFileUri()) : $image->getFileUri();
291        }
292      }
293      $avatars[] = $avatar;
294    }
295
296    if (\count($avatars) === 0) {
297      return [];
298    }
299
300    return [
301      '#type' => 'html_tag',
302      '#tag' => 'span',
303      '#attributes' => [
304        'class' => 'sl-avatar-group',
305      ],
306      'avatars' => $avatars,
307    ];
308  }
309
310  /**
311   * Rebuilds the island with the given builder ID.
312   *
313   * @param string $builder_id
314   *   The ID of the builder.
315   *
316   * @return array
317   *   The rebuilt island.
318   */
319  private function rebuild(string $builder_id): array {
320    if (!$this->builder) {
321      // @todo pass \Drupal\display_builder\InstanceInterface object in
322      // parameters instead of loading again.
323      /** @var \Drupal\display_builder\InstanceInterface $builder */
324      $builder = $this->entityTypeManager->getStorage('display_builder_instance')->load($builder_id);
325      $this->builder = $builder;
326    }
327
328    return $this->addOutOfBand(
329      $this->build($this->builder),
330      '#' . $this->getHtmlId($builder_id),
331      'innerHTML'
332    );
333  }
334
335}

Branches

Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once. Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

Collaboration->alterRenderable
171  public function alterRenderable(InstanceInterface $instance, array $build): array {
172    $build['#attributes'] = [
173      'hx-ext' => 'sse',
174      'sse-connect' => Url::fromRoute('display_builder.api_sse', ['display_builder_instance' => (string) $instance->id()])->toString(),
175    ];
176    // We don't attach it from the ::build() because the island doesn't
177    // always render something in the toolbar.
178    $build['#attached']['library'][] = 'display_builder/htmx_sse';
179
180    return $build;
Collaboration->build
133  public function build(InstanceInterface $builder, array $data = [], array $options = []): array {
134    $builder_id = (string) $builder->id();
135    // @todo pass \Drupal\display_builder\InstanceInterface object in
136    // parameters instead of loading again.
137    /** @var \Drupal\display_builder\InstanceInterface $builder */
138    $builder = $this->entityTypeManager->getStorage('display_builder_instance')->load($builder_id);
139    $this->builder = $builder;
140    $users = $builder->getUsers();
141    $current_user = $this->currentUser->id();
142    $users = $this->removeInactiveUsers($users);
143
144    // If no users (this situation must not happen), don't show anything.
145    if (\count($users) === 0) {
146      return [];
149    if (\array_key_exists($current_user, $users)) {
151      if (\count($users) === 1) {
152        return [];
155      $users = [$current_user => $users[$current_user]] + $users;
156
157      return $this->buildRenderable($users);
161    if (\count($users) === 1) {
162      $users = [$current_user => NULL] + $users;
163    }
164
165    return $this->buildRenderable($users);
165    return $this->buildRenderable($users);
Collaboration->buildConfigurationForm
80  public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
81    $options = [
82      '' => $this->t('- None -'),
83    ];
84    $configuration = $this->getConfiguration();
85
86    $fields = $this->entityFieldManager->getFieldDefinitions('user', 'user');
87
88    foreach ($fields as $field_id => $field) {
88    foreach ($fields as $field_id => $field) {
88    foreach ($fields as $field_id => $field) {
89      if ($field->getType() === 'image') {
88    foreach ($fields as $field_id => $field) {
89      if ($field->getType() === 'image') {
90        $options[$field_id] = $field->getLabel();
88    foreach ($fields as $field_id => $field) {
89      if ($field->getType() === 'image') {
90        $options[$field_id] = $field->getLabel();
91      }
92    }
93    $form['image_field'] = [
94      '#title' => $this->t('Image field'),
95      '#type' => 'select',
96      '#default_value' => $configuration['image_field'],
97      '#options' => $options,
98    ];
99
100    $styles = $this->entityTypeManager->getStorage('image_style')->loadMultiple();
101    $options = [
102      '' => $this->t('- None -'),
103    ];
104
105    foreach ($styles as $name => $style) {
105    foreach ($styles as $name => $style) {
105    foreach ($styles as $name => $style) {
105    foreach ($styles as $name => $style) {
106      $options[$name] = $style->label();
107    }
108    $form['image_style'] = [
109      '#title' => $this->t('Image style'),
110      '#type' => 'select',
111      '#default_value' => $configuration['image_style'],
112      '#options' => $options,
113    ];
114
115    return $form;
Collaboration->buildRenderable
253  protected function buildRenderable(array $users): array {
254    $avatars = [];
255    $configuration = $this->getConfiguration();
256
257    foreach ($users as $user_id => $time) {
257    foreach ($users as $user_id => $time) {
257    foreach ($users as $user_id => $time) {
258      /** @var \Drupal\user\UserInterface $user */
259      $user = $this->entityTypeManager->getStorage('user')->load($user_id);
260
261      if (!$user) {
263        continue;
266        '#type' => 'component',
267        '#component' => 'display_builder:avatar',
268        '#props' => [
269          'name' => $user->getDisplayName(),
270        ],
271        '#attributes' => [
272          'style' => '--size: 38px',
273        ],
274      ];
275
276      if ($time) {
280        $time = $this->dateFormatter->format($time, 'custom', 'G:i');
281        $avatar['#props']['name'] .= ', ' . $this->t('at @time', ['@time' => $time]);
282      }
283
284      if (isset($configuration['image_field']) && $user->hasField($configuration['image_field'])) {
284      if (isset($configuration['image_field']) && $user->hasField($configuration['image_field'])) {
284      if (isset($configuration['image_field']) && $user->hasField($configuration['image_field'])) {
285        $image = $user->get($configuration['image_field'])->entity;
286
287        if ($image instanceof FileInterface) {
288          $image_style = $configuration['image_style'];
289          $style = $this->entityTypeManager->getStorage('image_style')->load($image_style);
290          $avatar['#props']['image'] = $style ? $style->buildUri($image->getFileUri()) : $image->getFileUri();
290          $avatar['#props']['image'] = $style ? $style->buildUri($image->getFileUri()) : $image->getFileUri();
290          $avatar['#props']['image'] = $style ? $style->buildUri($image->getFileUri()) : $image->getFileUri();
290          $avatar['#props']['image'] = $style ? $style->buildUri($image->getFileUri()) : $image->getFileUri();
291        }
292      }
293      $avatars[] = $avatar;
257    foreach ($users as $user_id => $time) {
258      /** @var \Drupal\user\UserInterface $user */
259      $user = $this->entityTypeManager->getStorage('user')->load($user_id);
260
261      if (!$user) {
262        // For example, if the user was deleted.
263        continue;
264      }
265      $avatar = [
266        '#type' => 'component',
267        '#component' => 'display_builder:avatar',
268        '#props' => [
269          'name' => $user->getDisplayName(),
270        ],
271        '#attributes' => [
272          'style' => '--size: 38px',
273        ],
274      ];
275
276      if ($time) {
277        // We can't use DateFormatterInterface::formatTimeDiffSince() because
278        // the displayed value will become obsolete if the island is not updated
279        // for a while.
280        $time = $this->dateFormatter->format($time, 'custom', 'G:i');
281        $avatar['#props']['name'] .= ', ' . $this->t('at @time', ['@time' => $time]);
282      }
283
284      if (isset($configuration['image_field']) && $user->hasField($configuration['image_field'])) {
285        $image = $user->get($configuration['image_field'])->entity;
286
287        if ($image instanceof FileInterface) {
288          $image_style = $configuration['image_style'];
289          $style = $this->entityTypeManager->getStorage('image_style')->load($image_style);
290          $avatar['#props']['image'] = $style ? $style->buildUri($image->getFileUri()) : $image->getFileUri();
291        }
292      }
293      $avatars[] = $avatar;
257    foreach ($users as $user_id => $time) {
258      /** @var \Drupal\user\UserInterface $user */
259      $user = $this->entityTypeManager->getStorage('user')->load($user_id);
260
261      if (!$user) {
262        // For example, if the user was deleted.
263        continue;
264      }
265      $avatar = [
266        '#type' => 'component',
267        '#component' => 'display_builder:avatar',
268        '#props' => [
269          'name' => $user->getDisplayName(),
270        ],
271        '#attributes' => [
272          'style' => '--size: 38px',
273        ],
274      ];
275
276      if ($time) {
277        // We can't use DateFormatterInterface::formatTimeDiffSince() because
278        // the displayed value will become obsolete if the island is not updated
279        // for a while.
280        $time = $this->dateFormatter->format($time, 'custom', 'G:i');
281        $avatar['#props']['name'] .= ', ' . $this->t('at @time', ['@time' => $time]);
282      }
283
284      if (isset($configuration['image_field']) && $user->hasField($configuration['image_field'])) {
285        $image = $user->get($configuration['image_field'])->entity;
286
287        if ($image instanceof FileInterface) {
288          $image_style = $configuration['image_style'];
289          $style = $this->entityTypeManager->getStorage('image_style')->load($image_style);
290          $avatar['#props']['image'] = $style ? $style->buildUri($image->getFileUri()) : $image->getFileUri();
291        }
292      }
293      $avatars[] = $avatar;
294    }
295
296    if (\count($avatars) === 0) {
297      return [];
301      '#type' => 'html_tag',
302      '#tag' => 'span',
303      '#attributes' => [
304        'class' => 'sl-avatar-group',
305      ],
306      'avatars' => $avatars,
Collaboration->configurationSummary
122    $conf = $this->getConfiguration();
123    $with_picture_text = $conf['image_style'] ? $this->t('With @style picture.', ['@style' => $conf['image_style']]) : $this->t('With picture');
123    $with_picture_text = $conf['image_style'] ? $this->t('With @style picture.', ['@style' => $conf['image_style']]) : $this->t('With picture');
123    $with_picture_text = $conf['image_style'] ? $this->t('With @style picture.', ['@style' => $conf['image_style']]) : $this->t('With picture');
123    $with_picture_text = $conf['image_style'] ? $this->t('With @style picture.', ['@style' => $conf['image_style']]) : $this->t('With picture');
124
125    return [
126      $conf['image_field'] ? $with_picture_text : $this->t('Without picture.'),
126      $conf['image_field'] ? $with_picture_text : $this->t('Without picture.'),
126      $conf['image_field'] ? $with_picture_text : $this->t('Without picture.'),
126      $conf['image_field'] ? $with_picture_text : $this->t('Without picture.'),
Collaboration->create
58  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
59    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
60    $instance->currentUser = $container->get('current_user');
61    $instance->dateFormatter = $container->get('date.formatter');
62    $instance->entityFieldManager = $container->get('entity_field.manager');
63
64    return $instance;
Collaboration->defaultConfiguration
72      'image_field' => 'user_picture',
Collaboration->onAttachToRoot
186  public function onAttachToRoot(string $builder_id, string $instance_id): array {
187    return $this->rebuild($builder_id);
Collaboration->onAttachToSlot
193  public function onAttachToSlot(string $builder_id, string $instance_id, string $parent_id): array {
194    return $this->rebuild($builder_id);
Collaboration->onDelete
221  public function onDelete(string $builder_id, string $parent_id): array {
222    return $this->rebuild($builder_id);
Collaboration->onHistoryChange
207  public function onHistoryChange(string $builder_id): array {
208    return $this->rebuild($builder_id);
Collaboration->onMove
200  public function onMove(string $builder_id, string $instance_id): array {
201    return $this->rebuild($builder_id);
Collaboration->onUpdate
214  public function onUpdate(string $builder_id, string $instance_id): array {
215    return $this->rebuild($builder_id);
Collaboration->rebuild
319  private function rebuild(string $builder_id): array {
320    if (!$this->builder) {
324      $builder = $this->entityTypeManager->getStorage('display_builder_instance')->load($builder_id);
325      $this->builder = $builder;
326    }
327
328    return $this->addOutOfBand(
328    return $this->addOutOfBand(
329      $this->build($this->builder),
330      '#' . $this->getHtmlId($builder_id),
331      'innerHTML'
Collaboration->removeInactiveUsers
234  protected function removeInactiveUsers(array $users): array {
235    foreach ($users as $user_id => $time) {
235    foreach ($users as $user_id => $time) {
235    foreach ($users as $user_id => $time) {
236      if (\time() - $time > self::SECONDS_IN_15_MINUTES) {
235    foreach ($users as $user_id => $time) {
236      if (\time() - $time > self::SECONDS_IN_15_MINUTES) {
237        unset($users[$user_id]);
235    foreach ($users as $user_id => $time) {
236      if (\time() - $time > self::SECONDS_IN_15_MINUTES) {
237        unset($users[$user_id]);
238      }
239    }
240
241    return $users;