xref: /webtrees/app/Module/StoriesModule.php (revision 7413816e6dd2d50e569034fb804f3dce7471bb94)
18c2e8227SGreg Roach<?php
23976b470SGreg Roach
38c2e8227SGreg Roach/**
48c2e8227SGreg Roach * webtrees: online genealogy
5d11be702SGreg Roach * Copyright (C) 2023 webtrees development team
68c2e8227SGreg Roach * This program is free software: you can redistribute it and/or modify
78c2e8227SGreg Roach * it under the terms of the GNU General Public License as published by
88c2e8227SGreg Roach * the Free Software Foundation, either version 3 of the License, or
98c2e8227SGreg Roach * (at your option) any later version.
108c2e8227SGreg Roach * This program is distributed in the hope that it will be useful,
118c2e8227SGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
128c2e8227SGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
138c2e8227SGreg Roach * GNU General Public License for more details.
148c2e8227SGreg Roach * You should have received a copy of the GNU General Public License
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
168c2e8227SGreg Roach */
17fcfa147eSGreg Roach
18e7f56f2aSGreg Roachdeclare(strict_types=1);
19e7f56f2aSGreg Roach
2076692c8bSGreg Roachnamespace Fisharebest\Webtrees\Module;
2176692c8bSGreg Roach
220e62c4b8SGreg Roachuse Fisharebest\Webtrees\Auth;
236f4ec3caSGreg Roachuse Fisharebest\Webtrees\DB;
24e218f363SGreg Roachuse Fisharebest\Webtrees\Http\RequestHandlers\ControlPanel;
250e62c4b8SGreg Roachuse Fisharebest\Webtrees\I18N;
260e62c4b8SGreg Roachuse Fisharebest\Webtrees\Individual;
270e62c4b8SGreg Roachuse Fisharebest\Webtrees\Menu;
289f0bdfcdSGreg Roachuse Fisharebest\Webtrees\Registry;
2950d6f48cSGreg Roachuse Fisharebest\Webtrees\Services\HtmlService;
30e218f363SGreg Roachuse Fisharebest\Webtrees\Services\TreeService;
310e62c4b8SGreg Roachuse Fisharebest\Webtrees\Tree;
32b55cbc6bSGreg Roachuse Fisharebest\Webtrees\Validator;
336ccdf4f0SGreg Roachuse Psr\Http\Message\ResponseInterface;
346ccdf4f0SGreg Roachuse Psr\Http\Message\ServerRequestInterface;
3583d28054SGreg Roach
3610e06497SGreg Roachuse function in_array;
37e218f363SGreg Roachuse function redirect;
38e218f363SGreg Roachuse function route;
398c2e8227SGreg Roach
408c2e8227SGreg Roach/**
418c2e8227SGreg Roach * Class StoriesModule
428c2e8227SGreg Roach */
4337eb8894SGreg Roachclass StoriesModule extends AbstractModule implements ModuleConfigInterface, ModuleMenuInterface, ModuleTabInterface
44c1010edaSGreg Roach{
4549a243cbSGreg Roach    use ModuleTabTrait;
4649a243cbSGreg Roach    use ModuleConfigTrait;
4749a243cbSGreg Roach    use ModuleMenuTrait;
4849a243cbSGreg Roach
4943f2f523SGreg Roach    private HtmlService $html_service;
5050d6f48cSGreg Roach
5143f2f523SGreg Roach    private TreeService $tree_service;
52e218f363SGreg Roach
5350d6f48cSGreg Roach    /**
5450d6f48cSGreg Roach     * @param HtmlService $html_service
55e218f363SGreg Roach     * @param TreeService $tree_service
5650d6f48cSGreg Roach     */
57e218f363SGreg Roach    public function __construct(HtmlService $html_service, TreeService $tree_service)
5850d6f48cSGreg Roach    {
5950d6f48cSGreg Roach        $this->html_service = $html_service;
60e218f363SGreg Roach        $this->tree_service = $tree_service;
6150d6f48cSGreg Roach    }
6250d6f48cSGreg Roach
6349a243cbSGreg Roach    /** @var int The default access level for this module.  It can be changed in the control panel. */
6433c746f1SGreg Roach    protected int $access_level = Auth::PRIV_HIDE;
6549a243cbSGreg Roach
6649a243cbSGreg Roach    public function description(): string
67c1010edaSGreg Roach    {
68bbb76c12SGreg Roach        /* I18N: Description of the “Stories” module */
69bbb76c12SGreg Roach        return I18N::translate('Add narrative stories to individuals in the family tree.');
708c2e8227SGreg Roach    }
718c2e8227SGreg Roach
72aee13b6dSGreg Roach    /**
7349a243cbSGreg Roach     * The default position for this menu.  It can be changed in the control panel.
74aee13b6dSGreg Roach     *
7549a243cbSGreg Roach     * @return int
76aee13b6dSGreg Roach     */
7749a243cbSGreg Roach    public function defaultMenuOrder(): int
78c1010edaSGreg Roach    {
79353b36abSGreg Roach        return 7;
808c2e8227SGreg Roach    }
818c2e8227SGreg Roach
8249a243cbSGreg Roach    /**
8349a243cbSGreg Roach     * The default position for this tab.  It can be changed in the control panel.
8449a243cbSGreg Roach     *
8549a243cbSGreg Roach     * @return int
8649a243cbSGreg Roach     */
87cbf4b7faSGreg Roach    public function defaultTabOrder(): int
88cbf4b7faSGreg Roach    {
89fb7a0427SGreg Roach        return 9;
908c2e8227SGreg Roach    }
918c2e8227SGreg Roach
923caaa4d2SGreg Roach    /**
933caaa4d2SGreg Roach     * Generate the HTML content of this tab.
943caaa4d2SGreg Roach     *
953caaa4d2SGreg Roach     * @param Individual $individual
963caaa4d2SGreg Roach     *
973caaa4d2SGreg Roach     * @return string
983caaa4d2SGreg Roach     */
999b34404bSGreg Roach    public function getTabContent(Individual $individual): string
100c1010edaSGreg Roach    {
10172ac996dSGreg Roach        return view('modules/stories/tab', [
10272ac996dSGreg Roach            'is_admin'   => Auth::isAdmin(),
103225e381fSGreg Roach            'individual' => $individual,
104225e381fSGreg Roach            'stories'    => $this->getStoriesForIndividual($individual),
1050eb8cd1eSGreg Roach            'tree'       => $individual->tree(),
106225e381fSGreg Roach        ]);
1078c2e8227SGreg Roach    }
1088c2e8227SGreg Roach
109225e381fSGreg Roach    /**
110225e381fSGreg Roach     * @param Individual $individual
111225e381fSGreg Roach     *
112f70bcff5SGreg Roach     * @return array<object>
113225e381fSGreg Roach     */
114c1010edaSGreg Roach    private function getStoriesForIndividual(Individual $individual): array
115c1010edaSGreg Roach    {
1164b92b602SGreg Roach        $block_ids = DB::table('block')
11726684e68SGreg Roach            ->where('module_name', '=', $this->name())
1184b92b602SGreg Roach            ->where('xref', '=', $individual->xref())
1194b92b602SGreg Roach            ->where('gedcom_id', '=', $individual->tree()->id())
1204b92b602SGreg Roach            ->pluck('block_id');
121225e381fSGreg Roach
122225e381fSGreg Roach        $stories = [];
123225e381fSGreg Roach        foreach ($block_ids as $block_id) {
1247d988ec3SGreg Roach            $block_id = (int) $block_id;
1257d988ec3SGreg Roach
126225e381fSGreg Roach            // Only show this block for certain languages
12750d6f48cSGreg Roach            $languages = $this->getBlockSetting($block_id, 'languages');
12865cf5706SGreg Roach            if ($languages === '' || in_array(I18N::languageTag(), explode(',', $languages), true)) {
129225e381fSGreg Roach                $stories[] = (object) [
130225e381fSGreg Roach                    'block_id'   => $block_id,
131225e381fSGreg Roach                    'title'      => $this->getBlockSetting($block_id, 'title'),
13272ac996dSGreg Roach                    'story_body' => $this->getBlockSetting($block_id, 'story_body'),
133225e381fSGreg Roach                ];
134225e381fSGreg Roach            }
135225e381fSGreg Roach        }
136225e381fSGreg Roach
137225e381fSGreg Roach        return $stories;
1388c2e8227SGreg Roach    }
1398c2e8227SGreg Roach
1403caaa4d2SGreg Roach    /**
1413caaa4d2SGreg Roach     * Is this tab empty? If so, we don't always need to display it.
1423caaa4d2SGreg Roach     *
1433caaa4d2SGreg Roach     * @param Individual $individual
1443caaa4d2SGreg Roach     *
1453caaa4d2SGreg Roach     * @return bool
1463caaa4d2SGreg Roach     */
1476ccdf4f0SGreg Roach    public function hasTabContent(Individual $individual): bool
1486ccdf4f0SGreg Roach    {
14954c1ab5eSGreg Roach        return Auth::isManager($individual->tree()) || $this->getStoriesForIndividual($individual) !== [];
1506ccdf4f0SGreg Roach    }
1516ccdf4f0SGreg Roach
1523caaa4d2SGreg Roach    /**
1533caaa4d2SGreg Roach     * A greyed out tab has no actual content, but may perhaps have
1543caaa4d2SGreg Roach     * options to create content.
1553caaa4d2SGreg Roach     *
1563caaa4d2SGreg Roach     * @param Individual $individual
1573caaa4d2SGreg Roach     *
1583caaa4d2SGreg Roach     * @return bool
1593caaa4d2SGreg Roach     */
1606ccdf4f0SGreg Roach    public function isGrayedOut(Individual $individual): bool
1616ccdf4f0SGreg Roach    {
162602c43e8SCarmen        return $this->getStoriesForIndividual($individual) === [];
1636ccdf4f0SGreg Roach    }
1646ccdf4f0SGreg Roach
1653caaa4d2SGreg Roach    /**
1663caaa4d2SGreg Roach     * Can this tab load asynchronously?
1673caaa4d2SGreg Roach     *
1683caaa4d2SGreg Roach     * @return bool
1693caaa4d2SGreg Roach     */
1706ccdf4f0SGreg Roach    public function canLoadAjax(): bool
1716ccdf4f0SGreg Roach    {
1726ccdf4f0SGreg Roach        return false;
1736ccdf4f0SGreg Roach    }
1746ccdf4f0SGreg Roach
1758c2e8227SGreg Roach    /**
1760ee13198SGreg Roach     * A menu, to be added to the main application menu.
1770ee13198SGreg Roach     *
178aee13b6dSGreg Roach     * @param Tree $tree
179aee13b6dSGreg Roach     *
1800ee13198SGreg Roach     * @return Menu|null
1810ee13198SGreg Roach     */
1821ff45046SGreg Roach    public function getMenu(Tree $tree): Menu|null
183c1010edaSGreg Roach    {
184aa6311c7SGreg Roach        return new Menu($this->title(), route('module', [
18526684e68SGreg Roach            'module' => $this->name(),
186c1010edaSGreg Roach            'action' => 'ShowList',
1879022ab66SGreg Roach            'tree'    => $tree->name(),
188c1010edaSGreg Roach        ]), 'menu-story');
1898c2e8227SGreg Roach    }
19072ac996dSGreg Roach
19172ac996dSGreg Roach    /**
1926ccdf4f0SGreg Roach     * How should this module be identified in the control panel, etc.?
1936ccdf4f0SGreg Roach     *
1946ccdf4f0SGreg Roach     * @return string
1956ccdf4f0SGreg Roach     */
1966ccdf4f0SGreg Roach    public function title(): string
1976ccdf4f0SGreg Roach    {
1986ccdf4f0SGreg Roach        /* I18N: Name of a module */
1996ccdf4f0SGreg Roach        return I18N::translate('Stories');
2006ccdf4f0SGreg Roach    }
2016ccdf4f0SGreg Roach
2026ccdf4f0SGreg Roach    /**
20357ab2231SGreg Roach     * @param ServerRequestInterface $request
20472ac996dSGreg Roach     *
2056ccdf4f0SGreg Roach     * @return ResponseInterface
20672ac996dSGreg Roach     */
20757ab2231SGreg Roach    public function getAdminAction(ServerRequestInterface $request): ResponseInterface
208c1010edaSGreg Roach    {
20972ac996dSGreg Roach        $this->layout = 'layouts/administration';
21072ac996dSGreg Roach
211e218f363SGreg Roach        // This module can't run without a tree
212b55cbc6bSGreg Roach        $tree = Validator::attributes($request)->treeOptional();
213e218f363SGreg Roach
214e218f363SGreg Roach        if (!$tree instanceof Tree) {
215e218f363SGreg Roach            $tree = $this->tree_service->all()->first();
216e218f363SGreg Roach            if ($tree instanceof Tree) {
217e218f363SGreg Roach                return redirect(route('module', ['module' => $this->name(), 'action' => 'Admin', 'tree' => $tree->name()]));
218e218f363SGreg Roach            }
219e218f363SGreg Roach
220e218f363SGreg Roach            return redirect(route(ControlPanel::class));
221e218f363SGreg Roach        }
22257ab2231SGreg Roach
2234b92b602SGreg Roach        $stories = DB::table('block')
22426684e68SGreg Roach            ->where('module_name', '=', $this->name())
2254b92b602SGreg Roach            ->where('gedcom_id', '=', $tree->id())
2264b92b602SGreg Roach            ->orderBy('xref')
2274b92b602SGreg Roach            ->get();
22872ac996dSGreg Roach
22972ac996dSGreg Roach        foreach ($stories as $story) {
2305db543e1SGreg Roach            $block_id = (int) $story->block_id;
23164d90241SGreg Roach            $xref     = (string) $story->xref;
2325db543e1SGreg Roach
2336b9cb339SGreg Roach            $story->individual = Registry::individualFactory()->make($xref, $tree);
2345db543e1SGreg Roach            $story->title      = $this->getBlockSetting($block_id, 'title');
2355db543e1SGreg Roach            $story->languages  = $this->getBlockSetting($block_id, 'languages');
23672ac996dSGreg Roach        }
23772ac996dSGreg Roach
238f25fc0f9SGreg Roach        $tree_names = $this->tree_service->all()
239f25fc0f9SGreg Roach            ->map(static fn (Tree $tree): string => $tree->title());
2401e653452SGreg Roach
24172ac996dSGreg Roach        return $this->viewResponse('modules/stories/config', [
24271378461SGreg Roach            'module'     => $this->name(),
24372ac996dSGreg Roach            'stories'    => $stories,
24449a243cbSGreg Roach            'title'      => $this->title() . ' — ' . $tree->title(),
24572ac996dSGreg Roach            'tree'       => $tree,
2461e653452SGreg Roach            'tree_names' => $tree_names,
24772ac996dSGreg Roach        ]);
24872ac996dSGreg Roach    }
24972ac996dSGreg Roach
25072ac996dSGreg Roach    /**
2516ccdf4f0SGreg Roach     * @param ServerRequestInterface $request
25272ac996dSGreg Roach     *
2536ccdf4f0SGreg Roach     * @return ResponseInterface
25472ac996dSGreg Roach     */
255e218f363SGreg Roach    public function postAdminAction(ServerRequestInterface $request): ResponseInterface
256e218f363SGreg Roach    {
257e218f363SGreg Roach        return redirect(route('module', [
258e218f363SGreg Roach            'module' => $this->name(),
259e218f363SGreg Roach            'action' => 'Admin',
260748dbe15SGreg Roach            'tree'   => Validator::parsedBody($request)->string('tree'),
261e218f363SGreg Roach        ]));
262e218f363SGreg Roach    }
263e218f363SGreg Roach
264e218f363SGreg Roach    /**
265e218f363SGreg Roach     * @param ServerRequestInterface $request
266e218f363SGreg Roach     *
267e218f363SGreg Roach     * @return ResponseInterface
268e218f363SGreg Roach     */
26957ab2231SGreg Roach    public function getAdminEditAction(ServerRequestInterface $request): ResponseInterface
270c1010edaSGreg Roach    {
27172ac996dSGreg Roach        $this->layout = 'layouts/administration';
27272ac996dSGreg Roach
273b55cbc6bSGreg Roach        $tree     = Validator::attributes($request)->tree();
2749f0bdfcdSGreg Roach        $block_id = Validator::queryParams($request)->integer('block_id', 0);
2759f0bdfcdSGreg Roach        $url      = Validator::queryParams($request)->string('url', '');
276c532e5bbSGreg Roach
27772ac996dSGreg Roach        if ($block_id === 0) {
27872ac996dSGreg Roach            // Creating a new story
27972ac996dSGreg Roach            $story_title = '';
28072ac996dSGreg Roach            $story_body  = '';
28172ac996dSGreg Roach            $languages   = [];
282c03e33f5SGreg Roach            $xref        = Validator::queryParams($request)->isXref()->string('xref', '');
283cc13d6d8SGreg Roach            $title       = I18N::translate('Add a story') . ' — ' . e($tree->title());
28472ac996dSGreg Roach        } else {
28572ac996dSGreg Roach            // Editing an existing story
2864b92b602SGreg Roach            $xref = (string) DB::table('block')
2874b92b602SGreg Roach                ->where('block_id', '=', $block_id)
2884b92b602SGreg Roach                ->value('xref');
28972ac996dSGreg Roach
29050d6f48cSGreg Roach            $story_title = $this->getBlockSetting($block_id, 'title');
29150d6f48cSGreg Roach            $story_body  = $this->getBlockSetting($block_id, 'story_body');
29272ac996dSGreg Roach            $languages   = explode(',', $this->getBlockSetting($block_id, 'languages'));
293cc13d6d8SGreg Roach            $title       = I18N::translate('Edit the story') . ' — ' . e($tree->title());
29472ac996dSGreg Roach        }
29572ac996dSGreg Roach
2966b9cb339SGreg Roach        $individual = Registry::individualFactory()->make($xref, $tree);
297453a3d0bSGreg Roach
29872ac996dSGreg Roach        return $this->viewResponse('modules/stories/edit', [
29972ac996dSGreg Roach            'block_id'    => $block_id,
30072ac996dSGreg Roach            'languages'   => $languages,
30172ac996dSGreg Roach            'story_body'  => $story_body,
30272ac996dSGreg Roach            'story_title' => $story_title,
30372ac996dSGreg Roach            'title'       => $title,
30472ac996dSGreg Roach            'tree'        => $tree,
305c532e5bbSGreg Roach            'url'         => $url,
30672ac996dSGreg Roach            'individual'  => $individual,
30772ac996dSGreg Roach        ]);
30872ac996dSGreg Roach    }
30972ac996dSGreg Roach
31072ac996dSGreg Roach    /**
3116ccdf4f0SGreg Roach     * @param ServerRequestInterface $request
31272ac996dSGreg Roach     *
3136ccdf4f0SGreg Roach     * @return ResponseInterface
31472ac996dSGreg Roach     */
31557ab2231SGreg Roach    public function postAdminEditAction(ServerRequestInterface $request): ResponseInterface
316c1010edaSGreg Roach    {
317b55cbc6bSGreg Roach        $tree        = Validator::attributes($request)->tree();
3189f0bdfcdSGreg Roach        $block_id    = Validator::queryParams($request)->integer('block_id', 0);
3199f0bdfcdSGreg Roach        $xref        = Validator::parsedBody($request)->string('xref');
3209f0bdfcdSGreg Roach        $story_body  = Validator::parsedBody($request)->string('story_body');
3219f0bdfcdSGreg Roach        $story_title = Validator::parsedBody($request)->string('story_title');
3229f0bdfcdSGreg Roach        $languages   = Validator::parsedBody($request)->array('languages');
3239f0bdfcdSGreg Roach        $default_url = route('module', ['module' => $this->name(), 'action' => 'Admin', 'tree' => $tree->name()]);
3249f0bdfcdSGreg Roach        $url         = Validator::parsedBody($request)->isLocalUrl()->string('url', $default_url);
32550d6f48cSGreg Roach        $story_body  = $this->html_service->sanitize($story_body);
32650d6f48cSGreg Roach
32772ac996dSGreg Roach        if ($block_id !== 0) {
3284b92b602SGreg Roach            DB::table('block')
3294b92b602SGreg Roach                ->where('block_id', '=', $block_id)
3304b92b602SGreg Roach                ->update([
3314b92b602SGreg Roach                    'gedcom_id' => $tree->id(),
33272ac996dSGreg Roach                    'xref'      => $xref,
33372ac996dSGreg Roach                ]);
33472ac996dSGreg Roach        } else {
3354b92b602SGreg Roach            DB::table('block')->insert([
3364b92b602SGreg Roach                'gedcom_id'   => $tree->id(),
33772ac996dSGreg Roach                'xref'        => $xref,
33826684e68SGreg Roach                'module_name' => $this->name(),
3394b92b602SGreg Roach                'block_order' => 0,
34072ac996dSGreg Roach            ]);
34172ac996dSGreg Roach
342*4c96e13dSGreg Roach            $block_id = DB::lastInsertId();
34372ac996dSGreg Roach        }
34472ac996dSGreg Roach
34572ac996dSGreg Roach        $this->setBlockSetting($block_id, 'story_body', $story_body);
34672ac996dSGreg Roach        $this->setBlockSetting($block_id, 'title', $story_title);
34772ac996dSGreg Roach        $this->setBlockSetting($block_id, 'languages', implode(',', $languages));
34872ac996dSGreg Roach
3496ccdf4f0SGreg Roach        return redirect($url);
35072ac996dSGreg Roach    }
35172ac996dSGreg Roach
35272ac996dSGreg Roach    /**
3536ccdf4f0SGreg Roach     * @param ServerRequestInterface $request
35472ac996dSGreg Roach     *
3556ccdf4f0SGreg Roach     * @return ResponseInterface
35672ac996dSGreg Roach     */
35757ab2231SGreg Roach    public function postAdminDeleteAction(ServerRequestInterface $request): ResponseInterface
358c1010edaSGreg Roach    {
359b55cbc6bSGreg Roach        $tree     = Validator::attributes($request)->tree();
360748dbe15SGreg Roach        $block_id = Validator::queryParams($request)->integer('block_id');
36172ac996dSGreg Roach
3624b92b602SGreg Roach        DB::table('block_setting')
3634b92b602SGreg Roach            ->where('block_id', '=', $block_id)
3644b92b602SGreg Roach            ->delete();
36572ac996dSGreg Roach
3664b92b602SGreg Roach        DB::table('block')
3674b92b602SGreg Roach            ->where('block_id', '=', $block_id)
3684b92b602SGreg Roach            ->delete();
36972ac996dSGreg Roach
370c1010edaSGreg Roach        $url = route('module', [
37126684e68SGreg Roach            'module' => $this->name(),
372c1010edaSGreg Roach            'action' => 'Admin',
3739022ab66SGreg Roach            'tree'    => $tree->name(),
374c1010edaSGreg Roach        ]);
37572ac996dSGreg Roach
3766ccdf4f0SGreg Roach        return redirect($url);
37772ac996dSGreg Roach    }
37872ac996dSGreg Roach
37972ac996dSGreg Roach    /**
38057ab2231SGreg Roach     * @param ServerRequestInterface $request
38172ac996dSGreg Roach     *
3826ccdf4f0SGreg Roach     * @return ResponseInterface
38372ac996dSGreg Roach     */
38457ab2231SGreg Roach    public function getShowListAction(ServerRequestInterface $request): ResponseInterface
385c1010edaSGreg Roach    {
386b55cbc6bSGreg Roach        $tree = Validator::attributes($request)->tree();
38757ab2231SGreg Roach
3884b92b602SGreg Roach        $stories = DB::table('block')
38926684e68SGreg Roach            ->where('module_name', '=', $this->name())
3904b92b602SGreg Roach            ->where('gedcom_id', '=', $tree->id())
3914b92b602SGreg Roach            ->get()
392f70bcff5SGreg Roach            ->map(function (object $story) use ($tree): object {
3935db543e1SGreg Roach                $block_id = (int) $story->block_id;
39464d90241SGreg Roach                $xref     = (string) $story->xref;
3955db543e1SGreg Roach
3966b9cb339SGreg Roach                $story->individual = Registry::individualFactory()->make($xref, $tree);
3975db543e1SGreg Roach                $story->title      = $this->getBlockSetting($block_id, 'title');
3985db543e1SGreg Roach                $story->languages  = $this->getBlockSetting($block_id, 'languages');
39972ac996dSGreg Roach
4004b92b602SGreg Roach                return $story;
401f25fc0f9SGreg Roach            })
402fceda430SGreg Roach                // Filter non-existent and private individuals.
403f25fc0f9SGreg Roach            ->filter(static fn (object $story): bool => $story->individual instanceof Individual && $story->individual->canShow())
40472ac996dSGreg Roach                // Filter foreign languages.
405f25fc0f9SGreg Roach            ->filter(static fn (object $story): bool => $story->languages === '' || in_array(I18N::languageTag(), explode(',', $story->languages), true));
40672ac996dSGreg Roach
40772ac996dSGreg Roach        return $this->viewResponse('modules/stories/list', [
40872ac996dSGreg Roach            'stories' => $stories,
40949a243cbSGreg Roach            'title'   => $this->title(),
410745968f0SGreg Roach            'tree'    => $tree,
41172ac996dSGreg Roach        ]);
41272ac996dSGreg Roach    }
4138c2e8227SGreg Roach}
414