Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions framework/core/js/src/admin/components/BasicsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@ export default class BasicsPage<CustomAttrs extends IPageAttrs = IPageAttrs> ext
setting: 'forum_title',
label: app.translator.trans('core.admin.basics.forum_title_heading'),
})
.registerSetting({
type: 'text',
setting: 'forum_short_name',
label: app.translator.trans('core.admin.basics.forum_short_name_heading'),
help: app.translator.trans('core.admin.basics.forum_short_name_text'),
maxlength: 16,
})
.registerSetting({
type: 'text',
setting: 'forum_description',
Expand Down
2 changes: 2 additions & 0 deletions framework/core/locale/core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ core:
avatar_driver_text: Select a driver that should be used for users' avatars when no user-uploaded avatar is available. By default, no avatar will be displayed when a user has not uploaded one.
forum_description_heading: Forum Description
forum_description_text: Enter a short sentence or two that describes your forum. This will appear in the meta tag and show up in search engines.
forum_short_name_heading: Short Name
forum_short_name_text: "Up to 16 characters, used where the forum title doesn't fit: under the forum's icon on a phone's home screen, and when someone adds the forum as a search engine in their browser. Without one, the forum title is used, cut short at a whole word if it is too long."
forum_title_heading: Forum Title
home_page_heading: Home Page
home_page_text: Choose the page which users will first see when they visit your forum.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/

namespace Flarum\Api\Controller;

use Flarum\Discussion\Discussion;
use Flarum\Http\RequestUtil;
use Flarum\Http\SlugManager;
use Flarum\Http\UrlGenerator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Arr;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

/**
* Discussions a browser suggests as someone types in its search box, in the
* OpenSearch suggestions format: the query, then titles, descriptions and
* addresses. Only discussions the visitor can see are suggested.
*
* Titles are matched on the start of their words, so a word is completed as
* it is typed. The forum's search matches whole words on MySQL, MariaDB and
* PostgreSQL, so it can't suggest anything until a word is finished.
*/
class ShowOpenSearchSuggestionsController implements RequestHandlerInterface
{
public const MIN_LENGTH = 2;

public const LIMIT = 8;

public function __construct(
protected SlugManager $slugs,
protected UrlGenerator $url
) {
}

public function handle(ServerRequestInterface $request): ResponseInterface
{
$query = trim((string) Arr::get($request->getQueryParams(), 'q', ''));
$words = preg_split('/[^\p{L}\p{N}\p{M}]+/u', $query, -1, PREG_SPLIT_NO_EMPTY) ?: [];
$titles = $descriptions = $urls = [];

if (mb_strlen(implode('', $words)) >= self::MIN_LENGTH) {
$discussions = Discussion::whereVisibleTo(RequestUtil::getActor($request))
->where(function (Builder $titles) use ($words) {
foreach ($words as $word) {
$titles->where(fn (Builder $title) => $title
->whereLike('discussions.title', "$word%")
->orWhereLike('discussions.title', "% $word%"));
}
})
->orderByDesc('discussions.last_posted_at')
->limit(self::LIMIT)
->get();

foreach ($discussions as $discussion) {
$titles[] = $discussion->title;
$descriptions[] = '';
$urls[] = $this->url->to('forum')->route('discussion', ['id' => $this->slugs->forResource(Discussion::class)->toSlug($discussion)]);
}
}

return new JsonResponse([$query, $titles, $descriptions, $urls], 200, [
'Content-Type' => 'application/x-suggestions+json',
'Cache-Control' => 'private, max-age=60',
], JsonResponse::DEFAULT_JSON_FLAGS | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
}
7 changes: 7 additions & 0 deletions framework/core/src/Api/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,13 @@
$route->toController(Controller\DeleteFaviconController::class)
);

// Discussions a browser suggests when the forum is its search engine
$map->get(
'/opensearch/suggestions',
'opensearch.suggestions',
$route->toController(Controller\ShowOpenSearchSuggestionsController::class)
);

// Upload an app icon
$map->post(
'/app-icon',
Expand Down
19 changes: 19 additions & 0 deletions framework/core/src/Forum/Branding/Branding.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,25 @@ public function faviconIcoUrl(): ?string
return $this->appIconUrls()['favicon.ico'] ?? null;
}

/**
* Where the favicon a page can show is stored on the assets disk: a PNG, or
* an uploaded .ico.
*/
public function faviconPath(): ?string
{
if ($this->theme->has('favicon')) {
return $this->theme->paths('favicon')['png'] ?? null;
}

if ($path = $this->settings->get('favicon_path')) {
return $path;
}

$appIcon = $this->theme->has('app_icon') ? $this->theme->paths('app_icon') : $this->files->paths('app_icon_paths');

return $appIcon['favicon.png'] ?? null;
}

public function faviconSvgUrl(): ?string
{
return $this->theme->urls('favicon')['svg'] ?? null;
Expand Down
54 changes: 54 additions & 0 deletions framework/core/src/Forum/Branding/ShortName.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/

namespace Flarum\Forum\Branding;

use Flarum\Settings\SettingsRepositoryInterface;

/**
* The forum's name where its full title doesn't fit: a browser's list of
* search engines, or under an icon on a phone's home screen.
*/
class ShortName
{
public const MAX_LENGTH = 16;

public function __construct(
protected SettingsRepositoryInterface $settings
) {
}

/**
* The short name an admin set, if any.
*/
public function setting(): ?string
{
$name = trim((string) $this->settings->get('forum_short_name'));

return $name === '' ? null : $name;
}

/**
* The short name, or else the forum title, cut at the last whole word
* that fits when it is too long.
*/
public function get(): string
{
$name = $this->setting() ?? trim((string) $this->settings->get('forum_title'));

if (mb_strlen($name) <= self::MAX_LENGTH) {
return $name;
}

$cut = mb_substr($name, 0, self::MAX_LENGTH + 1);
$space = mb_strrpos($cut, ' ');

return rtrim($space ? mb_substr($cut, 0, $space) : mb_substr($cut, 0, self::MAX_LENGTH));
}
}
8 changes: 8 additions & 0 deletions framework/core/src/Forum/Branding/ThemeImages.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ public function urls(string $image): array
return array_map(fn (string $path) => $this->assets->url($path), $this->sync()[$image]['paths'] ?? []);
}

/**
* @return array<string, string> name => path on the assets disk
*/
public function paths(string $image): array
{
return $this->sync()[$image]['paths'] ?? [];
}

/**
* The email copy of a theme image, as EmailLogo resolves it.
*
Expand Down
4 changes: 3 additions & 1 deletion framework/core/src/Forum/Branding/WebManifest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ public function __construct(
protected Branding $branding,
protected AppIcon $appIcon,
protected UrlGenerator $url,
protected Container $container
protected Container $container,
protected ShortName $shortName
) {
}

Expand All @@ -45,6 +46,7 @@ public function build(): array

$manifest = array_filter([
'name' => $this->settings->get('forum_title'),
'short_name' => $this->shortName->setting(),
'description' => $this->settings->get('forum_description'),
'start_url' => $base,
'scope' => $base,
Expand Down
37 changes: 37 additions & 0 deletions framework/core/src/Forum/Controller/OpenSearchController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/

namespace Flarum\Forum\Controller;

use Flarum\Forum\OpenSearch\OpenSearchDescription;
use Laminas\Diactoros\Response\TextResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

/**
* Served by Flarum at an address without an .xml extension, which some web
* server configurations, including the nginx one Flarum suggests, would
* otherwise only look for on disk.
*/
class OpenSearchController implements RequestHandlerInterface
{
public function __construct(
protected OpenSearchDescription $description
) {
}

public function handle(ServerRequestInterface $request): ResponseInterface
{
return new TextResponse($this->description->build(), 200, [
'Content-Type' => 'application/opensearchdescription+xml; charset=utf-8',
'Cache-Control' => 'public, max-age=3600',
]);
}
}
Loading
Loading