Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow admins to configure instance favicon and logo #30040

Merged
merged 14 commits into from May 6, 2024
Merged
7 changes: 7 additions & 0 deletions app/helpers/application_helper.rb
Expand Up @@ -232,6 +232,13 @@ def prerender_custom_emojis(html, custom_emojis, other_options = {})
EmojiFormatter.new(html, custom_emojis, other_options.merge(animate: prefers_autoplay?)).to_s
end

def site_icon_path(type, size = '48')
icon = SiteUpload.find_by(var: type)
return nil unless icon

icon.file.url(size)
end

private

def storage_host_var
Expand Down
4 changes: 4 additions & 0 deletions app/models/form/admin_settings.rb
Expand Up @@ -37,6 +37,8 @@ class Form::AdminSettings
status_page_url
captcha_enabled
authorized_fetch
app_icon
favicon
).freeze

INTEGER_KEYS = %i(
Expand All @@ -63,6 +65,8 @@ class Form::AdminSettings
UPLOAD_KEYS = %i(
thumbnail
mascot
app_icon
favicon
).freeze

OVERRIDEN_SETTINGS = {
Expand Down
8 changes: 8 additions & 0 deletions app/models/site_upload.rb
Expand Up @@ -19,7 +19,15 @@
class SiteUpload < ApplicationRecord
include Attachmentable

FAVICON_SIZES = [16, 32, 48].freeze
APPLE_ICON_SIZES = [57, 60, 72, 76, 114, 120, 144, 152, 167, 180, 1024].freeze
ANDROID_ICON_SIZES = [36, 48, 72, 96, 144, 192, 256, 384, 512].freeze
FawazFarid marked this conversation as resolved.
Show resolved Hide resolved

APP_ICON_SIZES = (APPLE_ICON_SIZES + ANDROID_ICON_SIZES).uniq.freeze

STYLES = {
app_icon: APP_ICON_SIZES.each_with_object({}) { |size, hash| hash[size.to_s.to_sym] = "#{size}x#{size}#" }.freeze,
favicon: FAVICON_SIZES.each_with_object({}) { |size, hash| hash[size.to_s.to_sym] = "#{size}x#{size}#" }.freeze,
Gargron marked this conversation as resolved.
Show resolved Hide resolved
thumbnail: {
'@1x': {
format: 'png',
Expand Down
20 changes: 6 additions & 14 deletions app/serializers/manifest_serializer.rb
@@ -1,21 +1,10 @@
# frozen_string_literal: true

class ManifestSerializer < ActiveModel::Serializer
include ApplicationHelper
include RoutingHelper
include ActionView::Helpers::TextHelper

ICON_SIZES = %w(
36
48
72
96
144
192
256
384
512
).freeze

attributes :id, :name, :short_name,
:icons, :theme_color, :background_color,
:display, :start_url, :scope,
Expand All @@ -37,9 +26,12 @@ def short_name
end

def icons
ICON_SIZES.map do |size|
SiteUpload::ANDROID_ICON_SIZES.map do |size|
src = site_icon_path('app_icon', size.to_i)
src = URI.join(root_url, src).to_s if src.present?

{
src: frontend_asset_url("icons/android-chrome-#{size}x#{size}.png"),
src: src || frontend_asset_url("icons/android-chrome-#{size}x#{size}.png"),
sizes: "#{size}x#{size}",
type: 'image/png',
purpose: 'any maskable',
Expand Down
28 changes: 28 additions & 0 deletions app/views/admin/settings/appearance/show.html.haml
Expand Up @@ -23,6 +23,34 @@
input_html: { rows: 8 },
wrapper: :with_block_label

.fields-row
.fields-row__column.fields-row__column-6.fields-group
= f.input :favicon,
as: :file,
input_html: { accept: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].join(',') },
wrapper: :with_block_label

.fields-row__column.fields-row__column-6.fields-group
- if @admin_settings.favicon.persisted?
= image_tag @admin_settings.favicon.file.url('48'), class: 'fields-group__thumbnail'
= link_to admin_site_upload_path(@admin_settings.favicon), data: { method: :delete }, class: 'link-button link-button--destructive' do
= fa_icon 'trash fw'
= t('admin.site_uploads.delete')

.fields-row
.fields-row__column.fields-row__column-6.fields-group
= f.input :app_icon,
as: :file,
input_html: { accept: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].join(',') },
wrapper: :with_block_label

.fields-row__column.fields-row__column-6.fields-group
- if @admin_settings.app_icon.persisted?
= image_tag @admin_settings.app_icon.file.url('48'), class: 'fields-group__thumbnail'
= link_to admin_site_upload_path(@admin_settings.app_icon), data: { method: :delete }, class: 'link-button link-button--destructive' do
= fa_icon 'trash fw'
= t('admin.site_uploads.delete')

.fields-row
.fields-row__column.fields-row__column-6.fields-group
= f.input :mascot,
Expand Down
10 changes: 5 additions & 5 deletions app/views/layouts/application.html.haml
Expand Up @@ -11,13 +11,13 @@
- if storage_host?
%link{ rel: 'dns-prefetch', href: storage_host }/

%link{ rel: 'icon', href: '/favicon.ico', type: 'image/x-icon' }/
%link{ rel: 'icon', href: site_icon_path('favicon') || '/favicon.ico', type: 'image/x-icon' }/

- %w(16 32 48).each do |size|
%link{ rel: 'icon', sizes: "#{size}x#{size}", href: frontend_asset_path("icons/favicon-#{size}x#{size}.png"), type: 'image/png' }/
- SiteUpload::FAVICON_SIZES.each do |size|
%link{ rel: 'icon', sizes: "#{size}x#{size}", href: site_icon_path('favicon', size.to_i) || frontend_asset_path("icons/favicon-#{size}x#{size}.png"), type: 'image/png' }/

- %w(57 60 72 76 114 120 144 152 167 180 1024).each do |size|
%link{ rel: 'apple-touch-icon', sizes: "#{size}x#{size}", href: frontend_asset_path("icons/apple-touch-icon-#{size}x#{size}.png") }/
- SiteUpload::APPLE_ICON_SIZES.each do |size|
%link{ rel: 'apple-touch-icon', sizes: "#{size}x#{size}", href: site_icon_path('app_icon', size.to_i) || frontend_asset_path("icons/apple-touch-icon-#{size}x#{size}.png") }/

%link{ rel: 'mask-icon', href: frontend_asset_path('images/logo-symbol-icon.svg'), color: '#6364FF' }/
%link{ rel: 'manifest', href: manifest_path(format: :json) }/
Expand Down
2 changes: 2 additions & 0 deletions config/locales/simple_form.en-GB.yml
Expand Up @@ -77,11 +77,13 @@ en-GB:
warn: Hide the filtered content behind a warning mentioning the filter's title
form_admin_settings:
activity_api_enabled: Counts of locally published posts, active users, and new registrations in weekly buckets
app_icon: WEBP, PNG, GIF or JPG. Overrides the default app icon on mobile devices with a custom icon.
backups_retention_period: Keep generated user archives for the specified number of days.
bootstrap_timeline_accounts: These accounts will be pinned to the top of new users' follow recommendations.
closed_registrations_message: Displayed when sign-ups are closed
content_cache_retention_period: Posts from other servers will be deleted after the specified number of days when set to a positive value. This may be irreversible.
custom_css: You can apply custom styles on the web version of Mastodon.
favicon: WEBP, PNG, GIF or JPG. Overrides the default Mastodon favicon with a custom icon.
mascot: Overrides the illustration in the advanced web interface.
media_cache_retention_period: Downloaded media files will be deleted after the specified number of days when set to a positive value, and re-downloaded on demand.
peers_api_enabled: A list of domain names this server has encountered in the fediverse. No data is included here about whether you federate with a given server, just that your server knows about it. This is used by services that collect statistics on federation in a general sense.
Expand Down
2 changes: 2 additions & 0 deletions config/locales/simple_form.en.yml
Expand Up @@ -77,11 +77,13 @@ en:
warn: Hide the filtered content behind a warning mentioning the filter's title
form_admin_settings:
activity_api_enabled: Counts of locally published posts, active users, and new registrations in weekly buckets
app_icon: WEBP, PNG, GIF or JPG. Overrides the default app icon on mobile devices with a custom icon.
backups_retention_period: Keep generated user archives for the specified number of days.
bootstrap_timeline_accounts: These accounts will be pinned to the top of new users' follow recommendations.
closed_registrations_message: Displayed when sign-ups are closed
content_cache_retention_period: All posts and boosts from other servers will be deleted after the specified number of days. Some posts may not be recoverable. All related bookmarks, favourites and boosts will also be lost and impossible to undo.
custom_css: You can apply custom styles on the web version of Mastodon.
favicon: WEBP, PNG, GIF or JPG. Overrides the default Mastodon favicon with a custom icon.
mascot: Overrides the illustration in the advanced web interface.
media_cache_retention_period: Downloaded media files will be deleted after the specified number of days when set to a positive value, and re-downloaded on demand.
peers_api_enabled: A list of domain names this server has encountered in the fediverse. No data is included here about whether you federate with a given server, just that your server knows about it. This is used by services that collect statistics on federation in a general sense.
Expand Down
24 changes: 24 additions & 0 deletions spec/helpers/application_helper_spec.rb
Expand Up @@ -285,4 +285,28 @@ def current_theme = 'default'
end
end
end

describe '#site_icon_path' do
context 'when an icon exists' do
let!(:favicon) { Fabricate(:site_upload, var: 'favicon') }

it 'returns the URL of the icon' do
expect(helper.site_icon_path('favicon')).to eq(favicon.file.url('48'))
end

it 'returns the URL of the icon with size parameter' do
expect(helper.site_icon_path('favicon', 16)).to eq(favicon.file.url('16'))
end
end

context 'when an icon does not exist' do
it 'returns nil' do
expect(helper.site_icon_path('favicon')).to be_nil
end

it 'returns nil with size parameter' do
expect(helper.site_icon_path('favicon', 16)).to be_nil
end
end
end
end