From a9ed85717163040fb202d49b0342e9e350cb12ba Mon Sep 17 00:00:00 2001 From: kibigo! Date: Sat, 18 Nov 2017 15:12:52 -0800 Subject: WIP static themeing --- app/javascript/core/about.js | 1 + app/javascript/core/admin.js | 42 ++++++++++++++++++++++++++ app/javascript/core/common.js | 5 ++++ app/javascript/core/embed.js | 23 +++++++++++++++ app/javascript/core/home.js | 1 + app/javascript/core/public.js | 1 + app/javascript/core/settings.js | 65 +++++++++++++++++++++++++++++++++++++++++ app/javascript/core/share.js | 1 + 8 files changed, 139 insertions(+) create mode 100644 app/javascript/core/about.js create mode 100644 app/javascript/core/admin.js create mode 100644 app/javascript/core/common.js create mode 100644 app/javascript/core/embed.js create mode 100644 app/javascript/core/home.js create mode 100644 app/javascript/core/public.js create mode 100644 app/javascript/core/settings.js create mode 100644 app/javascript/core/share.js (limited to 'app/javascript/core') diff --git a/app/javascript/core/about.js b/app/javascript/core/about.js new file mode 100644 index 000000000..6ed0e4ad3 --- /dev/null +++ b/app/javascript/core/about.js @@ -0,0 +1 @@ +// This file will be loaded on about pages, regardless of theme. diff --git a/app/javascript/core/admin.js b/app/javascript/core/admin.js new file mode 100644 index 000000000..c0bd09bdd --- /dev/null +++ b/app/javascript/core/admin.js @@ -0,0 +1,42 @@ +// This file will be loaded on admin pages, regardless of theme. + +import { delegate } from 'rails-ujs'; + +function handleDeleteStatus(event) { + const [data] = event.detail; + const element = document.querySelector(`[data-id="${data.id}"]`); + if (element) { + element.parentNode.removeChild(element); + } +} + +[].forEach.call(document.querySelectorAll('.trash-button'), (content) => { + content.addEventListener('ajax:success', handleDeleteStatus); +}); + +const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]'; + +delegate(document, '#batch_checkbox_all', 'change', ({ target }) => { + [].forEach.call(document.querySelectorAll(batchCheckboxClassName), (content) => { + content.checked = target.checked; + }); +}); + +delegate(document, batchCheckboxClassName, 'change', () => { + const checkAllElement = document.querySelector('#batch_checkbox_all'); + if (checkAllElement) { + checkAllElement.checked = [].every.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked); + } +}); + +delegate(document, '.media-spoiler-show-button', 'click', () => { + [].forEach.call(document.querySelectorAll('.activity-stream .media-spoiler-wrapper'), (content) => { + content.classList.add('media-spoiler-wrapper__visible'); + }); +}); + +delegate(document, '.media-spoiler-hide-button', 'click', () => { + [].forEach.call(document.querySelectorAll('.activity-stream .media-spoiler-wrapper'), (content) => { + content.classList.remove('media-spoiler-wrapper__visible'); + }); +}); diff --git a/app/javascript/core/common.js b/app/javascript/core/common.js new file mode 100644 index 000000000..24c0fdf61 --- /dev/null +++ b/app/javascript/core/common.js @@ -0,0 +1,5 @@ +// This file will be loaded on all pages, regardless of theme. + +import { start } from 'rails-ujs'; + +start(); diff --git a/app/javascript/core/embed.js b/app/javascript/core/embed.js new file mode 100644 index 000000000..8167706a3 --- /dev/null +++ b/app/javascript/core/embed.js @@ -0,0 +1,23 @@ +// This file will be loaded on embed pages, regardless of theme. + +window.addEventListener('message', e => { + const data = e.data || {}; + + if (!window.parent || data.type !== 'setHeight') { + return; + } + + function setEmbedHeight () { + window.parent.postMessage({ + type: 'setHeight', + id: data.id, + height: document.getElementsByTagName('html')[0].scrollHeight, + }, '*'); + }); + + if (['interactive', 'complete'].includes(document.readyState)) { + setEmbedHeight(); + } else { + document.addEventListener('DOMContentLoaded', setEmbedHeight); + } +}); diff --git a/app/javascript/core/home.js b/app/javascript/core/home.js new file mode 100644 index 000000000..3c2e01590 --- /dev/null +++ b/app/javascript/core/home.js @@ -0,0 +1 @@ +// This file will be loaded on home pages, regardless of theme. diff --git a/app/javascript/core/public.js b/app/javascript/core/public.js new file mode 100644 index 000000000..1a36b7a5f --- /dev/null +++ b/app/javascript/core/public.js @@ -0,0 +1 @@ +// This file will be loaded on public pages, regardless of theme. diff --git a/app/javascript/core/settings.js b/app/javascript/core/settings.js new file mode 100644 index 000000000..91332ed5a --- /dev/null +++ b/app/javascript/core/settings.js @@ -0,0 +1,65 @@ +// This file will be loaded on settings pages, regardless of theme. + +function main() { + const { length } = require('stringz'); + const { delegate } = require('rails-ujs'); + + delegate(document, '.webapp-btn', 'click', ({ target, button }) => { + if (button !== 0) { + return true; + } + window.location.href = target.href; + return false; + }); + + delegate(document, '.status__content__spoiler-link', 'click', ({ target }) => { + const contentEl = target.parentNode.parentNode.querySelector('.e-content'); + + if (contentEl.style.display === 'block') { + contentEl.style.display = 'none'; + target.parentNode.style.marginBottom = 0; + } else { + contentEl.style.display = 'block'; + target.parentNode.style.marginBottom = null; + } + + return false; + }); + + delegate(document, '.account_display_name', 'input', ({ target }) => { + const nameCounter = document.querySelector('.name-counter'); + + if (nameCounter) { + nameCounter.textContent = 30 - length(target.value); + } + }); + + delegate(document, '.account_note', 'input', ({ target }) => { + const noteCounter = document.querySelector('.note-counter'); + + if (noteCounter) { + const noteWithoutMetadata = processBio(target.value).text; + noteCounter.textContent = 500 - length(noteWithoutMetadata); + } + }); + + delegate(document, '#account_avatar', 'change', ({ target }) => { + const avatar = document.querySelector('.card.compact .avatar img'); + const [file] = target.files || []; + const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc; + + avatar.src = url; + }); + + delegate(document, '#account_header', 'change', ({ target }) => { + const header = document.querySelector('.card.compact'); + const [file] = target.files || []; + const url = file ? URL.createObjectURL(file) : header.dataset.originalSrc; + + header.style.backgroundImage = `url(${url})`; + }); +} + +loadPolyfills().then(main).catch(error => { + console.error(error); +}); diff --git a/app/javascript/core/share.js b/app/javascript/core/share.js new file mode 100644 index 000000000..98a413632 --- /dev/null +++ b/app/javascript/core/share.js @@ -0,0 +1 @@ +// This file will be loaded on share pages, regardless of theme. -- cgit From bdbbd06dad298dc3e1a5f568f4a3ff3635b48f22 Mon Sep 17 00:00:00 2001 From: kibigo! Date: Mon, 20 Nov 2017 22:13:37 -0800 Subject: Finalized theme loading and stuff --- app/controllers/about_controller.rb | 5 + app/controllers/accounts_controller.rb | 1 + app/controllers/admin/base_controller.rb | 7 +- app/controllers/application_controller.rb | 75 ++++++++++++-- app/controllers/auth/confirmations_controller.rb | 7 ++ app/controllers/auth/passwords_controller.rb | 5 + app/controllers/auth/registrations_controller.rb | 5 + app/controllers/auth/sessions_controller.rb | 5 + app/controllers/authorize_follows_controller.rb | 5 + app/controllers/follower_accounts_controller.rb | 4 +- app/controllers/following_accounts_controller.rb | 4 +- app/controllers/home_controller.rb | 5 + app/controllers/remote_follow_controller.rb | 5 + .../settings/applications_controller.rb | 4 +- app/controllers/settings/base_controller.rb | 12 +++ app/controllers/settings/deletes_controller.rb | 4 +- app/controllers/settings/exports_controller.rb | 6 +- .../settings/follower_domains_controller.rb | 6 +- app/controllers/settings/imports_controller.rb | 5 +- .../settings/keyword_mutes_controller.rb | 5 +- .../settings/notifications_controller.rb | 6 +- app/controllers/settings/preferences_controller.rb | 6 +- app/controllers/settings/profiles_controller.rb | 5 +- app/controllers/settings/sessions_controller.rb | 1 + .../two_factor_authentications_controller.rb | 5 +- app/controllers/shares_controller.rb | 5 + app/controllers/statuses_controller.rb | 2 + app/controllers/stream_entries_controller.rb | 1 + app/controllers/tags_controller.rb | 1 + app/javascript/core/about.js | 1 - app/javascript/core/embed.js | 2 +- app/javascript/core/home.js | 1 - app/javascript/core/public.js | 24 +++++ app/javascript/core/settings.js | 80 +++++---------- app/javascript/core/share.js | 1 - app/javascript/core/theme.yml | 14 +++ app/javascript/locales/index.js | 9 ++ app/javascript/mastodon/locales/index.js | 10 +- app/javascript/packs/about.js | 22 ++++ app/javascript/packs/application.js | 10 -- app/javascript/packs/common.js | 3 + app/javascript/packs/public.js | 75 ++++++++++++++ app/javascript/packs/share.js | 22 ++++ app/javascript/styles/common.scss | 5 - app/javascript/styles/win95.scss | 113 +++++++++++---------- .../themes/glitch/containers/mastodon.js | 2 +- app/javascript/themes/glitch/packs/common.js | 4 +- app/javascript/themes/glitch/packs/home.js | 4 +- app/javascript/themes/glitch/packs/public.js | 83 +-------------- app/javascript/themes/glitch/theme.yml | 39 ++++--- app/javascript/themes/vanilla/theme.yml | 35 ++++--- app/javascript/themes/win95/index.js | 10 ++ app/javascript/themes/win95/theme.yml | 23 +++++ app/lib/themes.rb | 12 +++ app/views/about/more.html.haml | 1 - app/views/about/show.html.haml | 1 - app/views/admin/reports/show.html.haml | 3 - app/views/admin/statuses/index.html.haml | 3 - app/views/home/index.html.haml | 6 -- app/views/layouts/_theme.html.haml | 10 ++ app/views/layouts/admin.html.haml | 3 - app/views/layouts/application.html.haml | 10 +- app/views/layouts/auth.html.haml | 3 - app/views/layouts/embedded.html.haml | 7 +- app/views/layouts/error.html.haml | 4 +- app/views/layouts/modal.html.haml | 3 - app/views/layouts/public.html.haml | 3 - app/views/shares/show.html.haml | 1 - app/views/tags/show.html.haml | 1 - config/webpack/configuration.js | 13 ++- config/webpack/generateLocalePacks.js | 2 +- config/webpack/shared.js | 40 ++++---- config/webpacker.yml | 12 --- 73 files changed, 566 insertions(+), 371 deletions(-) create mode 100644 app/controllers/settings/base_controller.rb delete mode 100644 app/javascript/core/about.js delete mode 100644 app/javascript/core/home.js delete mode 100644 app/javascript/core/share.js create mode 100644 app/javascript/core/theme.yml create mode 100644 app/javascript/locales/index.js create mode 100644 app/javascript/packs/about.js create mode 100644 app/javascript/packs/common.js create mode 100644 app/javascript/packs/public.js create mode 100644 app/javascript/packs/share.js delete mode 100644 app/javascript/styles/common.scss create mode 100644 app/javascript/themes/win95/index.js create mode 100644 app/javascript/themes/win95/theme.yml create mode 100644 app/views/layouts/_theme.html.haml (limited to 'app/javascript/core') diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 47690e81e..8785df14e 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class AboutController < ApplicationController + before_action :set_pack before_action :set_body_classes before_action :set_instance_presenter, only: [:show, :more, :terms] @@ -21,6 +22,10 @@ class AboutController < ApplicationController helper_method :new_user + def set_pack + use_pack action_name == 'show' ? 'about' : 'common' + end + def set_instance_presenter @instance_presenter = InstancePresenter.new end diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 75915b337..309cb65da 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -7,6 +7,7 @@ class AccountsController < ApplicationController def show respond_to do |format| format.html do + use_pack 'public' @pinned_statuses = [] if current_account && @account.blocking?(current_account) diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb index db4839a8f..726134509 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -4,8 +4,13 @@ module Admin class BaseController < ApplicationController include Authorization + layout 'admin' + before_action :require_staff! + before_action :set_pack - layout 'admin' + def set_pack + use_pack 'admin' + end end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index f5dbe837e..7cc4eea27 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -12,8 +12,6 @@ class ApplicationController < ActionController::Base helper_method :current_account helper_method :current_session - helper_method :current_theme - helper_method :theme_data helper_method :single_user_mode? rescue_from ActionController::RoutingError, with: :not_found @@ -54,6 +52,69 @@ class ApplicationController < ActionController::Base new_user_session_path end + def pack(data, pack_name) + return nil unless pack?(data, pack_name) + pack_data = { + common: pack_name == 'common' ? nil : resolve_pack(data['name'] ? Themes.instance.get(current_theme) : Themes.instance.core, 'common'), + name: data['name'], + pack: pack_name, + preload: nil, + stylesheet: false + } + if data['pack'][pack_name].is_a?(Hash) + pack_data[:common] = nil if data['pack'][pack_name]['use_common'] == false + pack_data[:pack] = nil unless data['pack'][pack_name]['filename'] + if data['pack'][pack_name]['preload'] + pack_data[:preload] = [data['pack'][pack_name]['preload']] if data['pack'][pack_name]['preload'].is_a?(String) + pack_data[:preload] = data['pack'][pack_name]['preload'] if data['pack'][pack_name]['preload'].is_a?(Array) + end + pack_data[:stylesheet] = true if data['pack'][pack_name]['stylesheet'] + end + pack_data + end + + def pack?(data, pack_name) + if data['pack'].is_a?(Hash) && data['pack'].key?(pack_name) + return true if data['pack'][pack_name].is_a?(String) || data['pack'][pack_name].is_a?(Hash) + end + false + end + + def nil_pack(data, pack_name) + { + common: pack_name == 'common' ? nil : resolve_pack(data['name'] ? Themes.instance.get(current_theme) : Themes.instance.core, 'common'), + name: data['name'], + pack: nil, + preload: nil, + stylesheet: false + } + end + + def resolve_pack(data, pack_name) + result = pack(data, pack_name) + unless result + if data['name'] && data.key?('fallback') + if data['fallback'].nil? + return nil_pack(data, pack_name) + elsif data['fallback'].is_a?(String) && Themes.instance.get(data['fallback']) + return resolve_pack(Themes.instance.get(data['fallback']), pack_name) + elsif data['fallback'].is_a?(Array) + data['fallback'].each do |fallback| + return resolve_pack(Themes.instance.get(fallback), pack_name) if Themes.instance.get(fallback) + end + end + return nil_pack(data, pack_name) + end + return data.key?('name') && data['name'] != default_theme ? resolve_pack(Themes.instance.get(default_theme), pack_name) : nil_pack(data, pack_name) + end + result + end + + def use_pack(pack_name) + @core = resolve_pack(Themes.instance.core, pack_name) + @theme = resolve_pack(Themes.instance.get(current_theme), pack_name) + end + protected def forbidden @@ -84,13 +145,13 @@ class ApplicationController < ActionController::Base @current_session ||= SessionActivation.find_by(session_id: cookies.signed['_session_id']) end - def current_theme - return Setting.default_settings['theme'] unless Themes.instance.names.include? current_user&.setting_theme - current_user.setting_theme + def default_theme + Setting.default_settings['theme'] end - def theme_data - Themes.instance.get(current_theme) + def current_theme + return default_theme unless Themes.instance.names.include? current_user&.setting_theme + current_user.setting_theme end def cache_collection(raw, klass) diff --git a/app/controllers/auth/confirmations_controller.rb b/app/controllers/auth/confirmations_controller.rb index d5e8e58ed..5ffa1c9a3 100644 --- a/app/controllers/auth/confirmations_controller.rb +++ b/app/controllers/auth/confirmations_controller.rb @@ -2,10 +2,17 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController layout 'auth' + before_action :set_pack def show super do |user| BootstrapTimelineWorker.perform_async(user.account_id) if user.errors.empty? end end + + private + + def set_pack + use_pack 'auth' + end end diff --git a/app/controllers/auth/passwords_controller.rb b/app/controllers/auth/passwords_controller.rb index 171b997dc..e0400aa3d 100644 --- a/app/controllers/auth/passwords_controller.rb +++ b/app/controllers/auth/passwords_controller.rb @@ -2,6 +2,7 @@ class Auth::PasswordsController < Devise::PasswordsController before_action :check_validity_of_reset_password_token, only: :edit + before_action :set_pack layout 'auth' @@ -17,4 +18,8 @@ class Auth::PasswordsController < Devise::PasswordsController def reset_password_token_is_valid? resource_class.with_reset_password_token(params[:reset_password_token]).present? end + + def set_pack + use_pack 'auth' + end end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 223db96ff..cc7a69ab0 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -5,6 +5,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController before_action :check_enabled_registrations, only: [:new, :create] before_action :configure_sign_up_params, only: [:create] + before_action :set_path before_action :set_sessions, only: [:edit, :update] before_action :set_instance_presenter, only: [:new, :create, :update] @@ -40,6 +41,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController private + def set_pack + use_pack %w(edit update).include?(action_name) ? 'admin' : 'auth' + end + def set_instance_presenter @instance_presenter = InstancePresenter.new end diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index a5acb6c36..72d544102 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -9,6 +9,7 @@ class Auth::SessionsController < Devise::SessionsController skip_before_action :check_suspension, only: [:destroy] prepend_before_action :authenticate_with_two_factor, if: :two_factor_enabled?, only: [:create] before_action :set_instance_presenter, only: [:new] + before_action :set_pack def create super do |resource| @@ -85,6 +86,10 @@ class Auth::SessionsController < Devise::SessionsController private + def set_pack + use_pack 'auth' + end + def set_instance_presenter @instance_presenter = InstancePresenter.new end diff --git a/app/controllers/authorize_follows_controller.rb b/app/controllers/authorize_follows_controller.rb index 78b564183..2d29bd379 100644 --- a/app/controllers/authorize_follows_controller.rb +++ b/app/controllers/authorize_follows_controller.rb @@ -4,6 +4,7 @@ class AuthorizeFollowsController < ApplicationController layout 'modal' before_action :authenticate_user! + before_action :set_pack def show @account = located_account || render(:error) @@ -23,6 +24,10 @@ class AuthorizeFollowsController < ApplicationController private + def set_pack + use_pack 'modal' + end + def follow_attempt FollowService.new.call(current_account, acct_without_prefix) end diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index 399e79665..080cbde11 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -7,7 +7,9 @@ class FollowerAccountsController < ApplicationController @follows = Follow.where(target_account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account) respond_to do |format| - format.html + format.html do + use_pack 'public' + end format.json do render json: collection_presenter, diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index 1e73d4bd4..74e83ad81 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -7,7 +7,9 @@ class FollowingAccountsController < ApplicationController @follows = Follow.where(account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account) respond_to do |format| - format.html + format.html do + use_pack 'public' + end format.json do render json: collection_presenter, diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 21dde20ce..7437a647e 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -2,6 +2,7 @@ class HomeController < ApplicationController before_action :authenticate_user! + before_action :set_pack before_action :set_initial_state_json def index @@ -37,6 +38,10 @@ class HomeController < ApplicationController redirect_to(default_redirect_path) end + def set_pack + use_pack 'home' + end + def set_initial_state_json serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer) @initial_state_json = serializable_resource.to_json diff --git a/app/controllers/remote_follow_controller.rb b/app/controllers/remote_follow_controller.rb index 48b026aa5..e6f379886 100644 --- a/app/controllers/remote_follow_controller.rb +++ b/app/controllers/remote_follow_controller.rb @@ -4,6 +4,7 @@ class RemoteFollowController < ApplicationController layout 'modal' before_action :set_account + before_action :set_pack before_action :gone, if: :suspended_account? def new @@ -31,6 +32,10 @@ class RemoteFollowController < ApplicationController { acct: session[:remote_follow] } end + def set_pack + use_pack 'modal' + end + def set_account @account = Account.find_local!(params[:account_username]) end diff --git a/app/controllers/settings/applications_controller.rb b/app/controllers/settings/applications_controller.rb index 8fc9a0fa9..35a6f7f9e 100644 --- a/app/controllers/settings/applications_controller.rb +++ b/app/controllers/settings/applications_controller.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true -class Settings::ApplicationsController < ApplicationController - layout 'admin' +class Settings::ApplicationsController < Settings::BaseController - before_action :authenticate_user! before_action :set_application, only: [:show, :update, :destroy, :regenerate] before_action :prepare_scopes, only: [:create, :update] diff --git a/app/controllers/settings/base_controller.rb b/app/controllers/settings/base_controller.rb new file mode 100644 index 000000000..7322d461b --- /dev/null +++ b/app/controllers/settings/base_controller.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class Settings::BaseController < ApplicationController + layout 'admin' + + before_action :authenticate_user! + before_action :set_pack + + def set_pack + use_pack 'settings' + end +end diff --git a/app/controllers/settings/deletes_controller.rb b/app/controllers/settings/deletes_controller.rb index 80002b995..e4cb35a8e 100644 --- a/app/controllers/settings/deletes_controller.rb +++ b/app/controllers/settings/deletes_controller.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true -class Settings::DeletesController < ApplicationController - layout 'admin' +class Settings::DeletesController < Settings::BaseController before_action :check_enabled_deletion - before_action :authenticate_user! def show @confirmation = Form::DeleteConfirmation.new diff --git a/app/controllers/settings/exports_controller.rb b/app/controllers/settings/exports_controller.rb index ae62f00c1..9c03ece86 100644 --- a/app/controllers/settings/exports_controller.rb +++ b/app/controllers/settings/exports_controller.rb @@ -1,10 +1,6 @@ # frozen_string_literal: true -class Settings::ExportsController < ApplicationController - layout 'admin' - - before_action :authenticate_user! - +class Settings::ExportsController < Settings::BaseController def show @export = Export.new(current_account) end diff --git a/app/controllers/settings/follower_domains_controller.rb b/app/controllers/settings/follower_domains_controller.rb index 9968504e5..141b2270d 100644 --- a/app/controllers/settings/follower_domains_controller.rb +++ b/app/controllers/settings/follower_domains_controller.rb @@ -2,11 +2,7 @@ require 'sidekiq-bulk' -class Settings::FollowerDomainsController < ApplicationController - layout 'admin' - - before_action :authenticate_user! - +class Settings::FollowerDomainsController < Settings::BaseController def show @account = current_account @domains = current_account.followers.reorder('MIN(follows.id) DESC').group('accounts.domain').select('accounts.domain, count(accounts.id) as accounts_from_domain').page(params[:page]).per(10) diff --git a/app/controllers/settings/imports_controller.rb b/app/controllers/settings/imports_controller.rb index 0db13d1ca..dbd136ebe 100644 --- a/app/controllers/settings/imports_controller.rb +++ b/app/controllers/settings/imports_controller.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true -class Settings::ImportsController < ApplicationController - layout 'admin' - - before_action :authenticate_user! +class Settings::ImportsController < Settings::BaseController before_action :set_account def show diff --git a/app/controllers/settings/keyword_mutes_controller.rb b/app/controllers/settings/keyword_mutes_controller.rb index f79e1b320..699b8a3ef 100644 --- a/app/controllers/settings/keyword_mutes_controller.rb +++ b/app/controllers/settings/keyword_mutes_controller.rb @@ -1,9 +1,6 @@ # frozen_string_literal: true -class Settings::KeywordMutesController < ApplicationController - layout 'admin' - - before_action :authenticate_user! +class Settings::KeywordMutesController < Settings::BaseController before_action :load_keyword_mute, only: [:edit, :update, :destroy] def index diff --git a/app/controllers/settings/notifications_controller.rb b/app/controllers/settings/notifications_controller.rb index ce2530c54..6286e3ebf 100644 --- a/app/controllers/settings/notifications_controller.rb +++ b/app/controllers/settings/notifications_controller.rb @@ -1,10 +1,6 @@ # frozen_string_literal: true -class Settings::NotificationsController < ApplicationController - layout 'admin' - - before_action :authenticate_user! - +class Settings::NotificationsController < Settings::BaseController def show; end def update diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 069026715..3aefd90a2 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -1,10 +1,6 @@ # frozen_string_literal: true -class Settings::PreferencesController < ApplicationController - layout 'admin' - - before_action :authenticate_user! - +class Settings::PreferencesController < Settings::BaseController def show; end def update diff --git a/app/controllers/settings/profiles_controller.rb b/app/controllers/settings/profiles_controller.rb index 28f78a4fb..dadc3d911 100644 --- a/app/controllers/settings/profiles_controller.rb +++ b/app/controllers/settings/profiles_controller.rb @@ -1,11 +1,8 @@ # frozen_string_literal: true -class Settings::ProfilesController < ApplicationController +class Settings::ProfilesController < Settings::BaseController include ObfuscateFilename - layout 'admin' - - before_action :authenticate_user! before_action :set_account obfuscate_filename [:account, :avatar] diff --git a/app/controllers/settings/sessions_controller.rb b/app/controllers/settings/sessions_controller.rb index 0da1b027b..780ea64b4 100644 --- a/app/controllers/settings/sessions_controller.rb +++ b/app/controllers/settings/sessions_controller.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# Intentionally does not inherit from BaseController class Settings::SessionsController < ApplicationController before_action :set_session, only: :destroy diff --git a/app/controllers/settings/two_factor_authentications_controller.rb b/app/controllers/settings/two_factor_authentications_controller.rb index 863cc7351..8c7737e9d 100644 --- a/app/controllers/settings/two_factor_authentications_controller.rb +++ b/app/controllers/settings/two_factor_authentications_controller.rb @@ -1,10 +1,7 @@ # frozen_string_literal: true module Settings - class TwoFactorAuthenticationsController < ApplicationController - layout 'admin' - - before_action :authenticate_user! + class TwoFactorAuthenticationsController < BaseController before_action :verify_otp_required, only: [:create] def show diff --git a/app/controllers/shares_controller.rb b/app/controllers/shares_controller.rb index 994742c3d..81d279c8b 100644 --- a/app/controllers/shares_controller.rb +++ b/app/controllers/shares_controller.rb @@ -4,6 +4,7 @@ class SharesController < ApplicationController layout 'modal' before_action :authenticate_user! + before_action :set_pack before_action :set_body_classes def show @@ -24,6 +25,10 @@ class SharesController < ApplicationController } end + def set_pack + use_pack 'share' + end + def set_body_classes @body_classes = 'compose-standalone' end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index e8a360fb5..84c9e7685 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -14,6 +14,7 @@ class StatusesController < ApplicationController def show respond_to do |format| format.html do + use_pack 'public' @ancestors = @status.reply? ? cache_collection(@status.ancestors(current_account), Status) : [] @descendants = cache_collection(@status.descendants(current_account), Status) @@ -37,6 +38,7 @@ class StatusesController < ApplicationController end def embed + use_pack 'embed' response.headers['X-Frame-Options'] = 'ALLOWALL' render 'stream_entries/embed', layout: 'embedded' end diff --git a/app/controllers/stream_entries_controller.rb b/app/controllers/stream_entries_controller.rb index 5f61e2182..b597ba4bb 100644 --- a/app/controllers/stream_entries_controller.rb +++ b/app/controllers/stream_entries_controller.rb @@ -14,6 +14,7 @@ class StreamEntriesController < ApplicationController def show respond_to do |format| format.html do + use_pack 'public' @ancestors = @stream_entry.activity.reply? ? cache_collection(@stream_entry.activity.ancestors(current_account), Status) : [] @descendants = cache_collection(@stream_entry.activity.descendants(current_account), Status) end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 9f3090e37..5d11a8139 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -9,6 +9,7 @@ class TagsController < ApplicationController respond_to do |format| format.html do + use_pack 'about' serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer) @initial_state_json = serializable_resource.to_json end diff --git a/app/javascript/core/about.js b/app/javascript/core/about.js deleted file mode 100644 index 6ed0e4ad3..000000000 --- a/app/javascript/core/about.js +++ /dev/null @@ -1 +0,0 @@ -// This file will be loaded on about pages, regardless of theme. diff --git a/app/javascript/core/embed.js b/app/javascript/core/embed.js index 8167706a3..6146e6592 100644 --- a/app/javascript/core/embed.js +++ b/app/javascript/core/embed.js @@ -13,7 +13,7 @@ window.addEventListener('message', e => { id: data.id, height: document.getElementsByTagName('html')[0].scrollHeight, }, '*'); - }); + }; if (['interactive', 'complete'].includes(document.readyState)) { setEmbedHeight(); diff --git a/app/javascript/core/home.js b/app/javascript/core/home.js deleted file mode 100644 index 3c2e01590..000000000 --- a/app/javascript/core/home.js +++ /dev/null @@ -1 +0,0 @@ -// This file will be loaded on home pages, regardless of theme. diff --git a/app/javascript/core/public.js b/app/javascript/core/public.js index 1a36b7a5f..47c34a259 100644 --- a/app/javascript/core/public.js +++ b/app/javascript/core/public.js @@ -1 +1,25 @@ // This file will be loaded on public pages, regardless of theme. + +const { delegate } = require('rails-ujs'); + +delegate(document, '.webapp-btn', 'click', ({ target, button }) => { + if (button !== 0) { + return true; + } + window.location.href = target.href; + return false; +}); + +delegate(document, '.status__content__spoiler-link', 'click', ({ target }) => { + const contentEl = target.parentNode.parentNode.querySelector('.e-content'); + + if (contentEl.style.display === 'block') { + contentEl.style.display = 'none'; + target.parentNode.style.marginBottom = 0; + } else { + contentEl.style.display = 'block'; + target.parentNode.style.marginBottom = null; + } + + return false; +}); diff --git a/app/javascript/core/settings.js b/app/javascript/core/settings.js index 91332ed5a..7fb1d8e77 100644 --- a/app/javascript/core/settings.js +++ b/app/javascript/core/settings.js @@ -1,65 +1,37 @@ // This file will be loaded on settings pages, regardless of theme. -function main() { - const { length } = require('stringz'); - const { delegate } = require('rails-ujs'); +const { length } = require('stringz'); +const { delegate } = require('rails-ujs'); - delegate(document, '.webapp-btn', 'click', ({ target, button }) => { - if (button !== 0) { - return true; - } - window.location.href = target.href; - return false; - }); +delegate(document, '.account_display_name', 'input', ({ target }) => { + const nameCounter = document.querySelector('.name-counter'); - delegate(document, '.status__content__spoiler-link', 'click', ({ target }) => { - const contentEl = target.parentNode.parentNode.querySelector('.e-content'); - - if (contentEl.style.display === 'block') { - contentEl.style.display = 'none'; - target.parentNode.style.marginBottom = 0; - } else { - contentEl.style.display = 'block'; - target.parentNode.style.marginBottom = null; - } - - return false; - }); - - delegate(document, '.account_display_name', 'input', ({ target }) => { - const nameCounter = document.querySelector('.name-counter'); - - if (nameCounter) { - nameCounter.textContent = 30 - length(target.value); - } - }); - - delegate(document, '.account_note', 'input', ({ target }) => { - const noteCounter = document.querySelector('.note-counter'); + if (nameCounter) { + nameCounter.textContent = 30 - length(target.value); + } +}); - if (noteCounter) { - const noteWithoutMetadata = processBio(target.value).text; - noteCounter.textContent = 500 - length(noteWithoutMetadata); - } - }); +delegate(document, '.account_note', 'input', ({ target }) => { + const noteCounter = document.querySelector('.note-counter'); - delegate(document, '#account_avatar', 'change', ({ target }) => { - const avatar = document.querySelector('.card.compact .avatar img'); - const [file] = target.files || []; - const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc; + if (noteCounter) { + const noteWithoutMetadata = processBio(target.value).text; + noteCounter.textContent = 500 - length(noteWithoutMetadata); + } +}); - avatar.src = url; - }); +delegate(document, '#account_avatar', 'change', ({ target }) => { + const avatar = document.querySelector('.card.compact .avatar img'); + const [file] = target.files || []; + const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc; - delegate(document, '#account_header', 'change', ({ target }) => { - const header = document.querySelector('.card.compact'); - const [file] = target.files || []; - const url = file ? URL.createObjectURL(file) : header.dataset.originalSrc; + avatar.src = url; +}); - header.style.backgroundImage = `url(${url})`; - }); -} +delegate(document, '#account_header', 'change', ({ target }) => { + const header = document.querySelector('.card.compact'); + const [file] = target.files || []; + const url = file ? URL.createObjectURL(file) : header.dataset.originalSrc; -loadPolyfills().then(main).catch(error => { - console.error(error); + header.style.backgroundImage = `url(${url})`; }); diff --git a/app/javascript/core/share.js b/app/javascript/core/share.js deleted file mode 100644 index 98a413632..000000000 --- a/app/javascript/core/share.js +++ /dev/null @@ -1 +0,0 @@ -// This file will be loaded on share pages, regardless of theme. diff --git a/app/javascript/core/theme.yml b/app/javascript/core/theme.yml new file mode 100644 index 000000000..17e8e66b3 --- /dev/null +++ b/app/javascript/core/theme.yml @@ -0,0 +1,14 @@ +# These packs will be loaded on every appropriate page, regardless of +# theme. +pack: + about: + admin: admin.js + auth: + common: common.js + embed: embed.js + error: + home: + modal: + public: public.js + settings: settings.js + share: diff --git a/app/javascript/locales/index.js b/app/javascript/locales/index.js new file mode 100644 index 000000000..421cb7fab --- /dev/null +++ b/app/javascript/locales/index.js @@ -0,0 +1,9 @@ +let theLocale; + +export function setLocale(locale) { + theLocale = locale; +} + +export function getLocale() { + return theLocale; +} diff --git a/app/javascript/mastodon/locales/index.js b/app/javascript/mastodon/locales/index.js index 421cb7fab..7e7297561 100644 --- a/app/javascript/mastodon/locales/index.js +++ b/app/javascript/mastodon/locales/index.js @@ -1,9 +1 @@ -let theLocale; - -export function setLocale(locale) { - theLocale = locale; -} - -export function getLocale() { - return theLocale; -} +export * from 'locales'; diff --git a/app/javascript/packs/about.js b/app/javascript/packs/about.js new file mode 100644 index 000000000..63e12da42 --- /dev/null +++ b/app/javascript/packs/about.js @@ -0,0 +1,22 @@ +import loadPolyfills from '../mastodon/load_polyfills'; + +function loaded() { + const TimelineContainer = require('../mastodon/containers/timeline_container').default; + const React = require('react'); + const ReactDOM = require('react-dom'); + const mountNode = document.getElementById('mastodon-timeline'); + + if (mountNode !== null) { + const props = JSON.parse(mountNode.getAttribute('data-props')); + ReactDOM.render(, mountNode); + } +} + +function main() { + const ready = require('../mastodon/ready').default; + ready(loaded); +} + +loadPolyfills().then(main).catch(error => { + console.error(error); +}); diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index ee5bf244c..116632dea 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -1,15 +1,5 @@ -// THIS IS THE `vanilla` THEME PACK FILE!! -// IT'S HERE FOR UPSTREAM COMPATIBILITY!! -// THE `glitch` PACK FILE IS IN `themes/glitch/index.js`!! - import loadPolyfills from '../mastodon/load_polyfills'; -// import default stylesheet with variables -import 'font-awesome/css/font-awesome.css'; -import '../styles/application.scss'; - -require.context('../images/', true); - loadPolyfills().then(() => { require('../mastodon/main').default(); }).catch(e => { diff --git a/app/javascript/packs/common.js b/app/javascript/packs/common.js new file mode 100644 index 000000000..f3156c1c6 --- /dev/null +++ b/app/javascript/packs/common.js @@ -0,0 +1,3 @@ +import 'font-awesome/css/font-awesome.css'; +import 'styles/application.scss' +require.context('../images/', true); diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js new file mode 100644 index 000000000..3472af6c1 --- /dev/null +++ b/app/javascript/packs/public.js @@ -0,0 +1,75 @@ +import loadPolyfills from '../mastodon/load_polyfills'; +import ready from '../mastodon/ready'; + +function main() { + const IntlRelativeFormat = require('intl-relativeformat').default; + const emojify = require('../mastodon/features/emoji/emoji').default; + const { getLocale } = require('../mastodon/locales'); + const { localeData } = getLocale(); + const VideoContainer = require('../mastodon/containers/video_container').default; + const MediaGalleryContainer = require('../mastodon/containers/media_gallery_container').default; + const CardContainer = require('../mastodon/containers/card_container').default; + const React = require('react'); + const ReactDOM = require('react-dom'); + + localeData.forEach(IntlRelativeFormat.__addLocaleData); + + ready(() => { + const locale = document.documentElement.lang; + + const dateTimeFormat = new Intl.DateTimeFormat(locale, { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + }); + + const relativeFormat = new IntlRelativeFormat(locale); + + [].forEach.call(document.querySelectorAll('.emojify'), (content) => { + content.innerHTML = emojify(content.innerHTML); + }); + + [].forEach.call(document.querySelectorAll('time.formatted'), (content) => { + const datetime = new Date(content.getAttribute('datetime')); + const formattedDate = dateTimeFormat.format(datetime); + + content.title = formattedDate; + content.textContent = formattedDate; + }); + + [].forEach.call(document.querySelectorAll('time.time-ago'), (content) => { + const datetime = new Date(content.getAttribute('datetime')); + + content.title = dateTimeFormat.format(datetime); + content.textContent = relativeFormat.format(datetime); + }); + + [].forEach.call(document.querySelectorAll('.logo-button'), (content) => { + content.addEventListener('click', (e) => { + e.preventDefault(); + window.open(e.target.href, 'mastodon-intent', 'width=400,height=400,resizable=no,menubar=no,status=no,scrollbars=yes'); + }); + }); + + [].forEach.call(document.querySelectorAll('[data-component="Video"]'), (content) => { + const props = JSON.parse(content.getAttribute('data-props')); + ReactDOM.render(, content); + }); + + [].forEach.call(document.querySelectorAll('[data-component="MediaGallery"]'), (content) => { + const props = JSON.parse(content.getAttribute('data-props')); + ReactDOM.render(, content); + }); + + [].forEach.call(document.querySelectorAll('[data-component="Card"]'), (content) => { + const props = JSON.parse(content.getAttribute('data-props')); + ReactDOM.render(, content); + }); + }); +} + +loadPolyfills().then(main).catch(error => { + console.error(error); +}); diff --git a/app/javascript/packs/share.js b/app/javascript/packs/share.js new file mode 100644 index 000000000..e9580f648 --- /dev/null +++ b/app/javascript/packs/share.js @@ -0,0 +1,22 @@ +import loadPolyfills from '../mastodon/load_polyfills'; + +function loaded() { + const ComposeContainer = require('../mastodon/containers/compose_container').default; + const React = require('react'); + const ReactDOM = require('react-dom'); + const mountNode = document.getElementById('mastodon-compose'); + + if (mountNode !== null) { + const props = JSON.parse(mountNode.getAttribute('data-props')); + ReactDOM.render(, mountNode); + } +} + +function main() { + const ready = require('../mastodon/ready').default; + ready(loaded); +} + +loadPolyfills().then(main).catch(error => { + console.error(error); +}); diff --git a/app/javascript/styles/common.scss b/app/javascript/styles/common.scss deleted file mode 100644 index c1772e7ae..000000000 --- a/app/javascript/styles/common.scss +++ /dev/null @@ -1,5 +0,0 @@ -// This makes our fonts available everywhere. - -@import 'fonts/roboto'; -@import 'fonts/roboto-mono'; -@import 'fonts/montserrat'; diff --git a/app/javascript/styles/win95.scss b/app/javascript/styles/win95.scss index 885837b53..6c89fc5bf 100644 --- a/app/javascript/styles/win95.scss +++ b/app/javascript/styles/win95.scss @@ -1,3 +1,8 @@ +// win95 theme from cybrespace. + +// Modified to inherit glitch styles (themes/glitch/styles/index.scss) +// instead of vanilla ones (./application.scss) + $win95-bg: #bfbfbf; $win95-dark-grey: #404040; $win95-mid-grey: #808080; @@ -17,7 +22,7 @@ $ui-highlight-color: $win95-window-header; } @mixin win95-outset() { - box-shadow: inset -1px -1px 0px #000000, + box-shadow: inset -1px -1px 0px #000000, inset 1px 1px 0px #ffffff, inset -2px -2px 0px #808080, inset 2px 2px 0px #dfdfdf; @@ -41,7 +46,7 @@ $ui-highlight-color: $win95-window-header; } @mixin win95-inset() { - box-shadow: inset 1px 1px 0px #000000, + box-shadow: inset 1px 1px 0px #000000, inset -1px -1px 0px #ffffff, inset 2px 2px 0px #808080, inset -2px -2px 0px #dfdfdf; @@ -51,7 +56,7 @@ $ui-highlight-color: $win95-window-header; @mixin win95-tab() { - box-shadow: inset -1px 0px 0px #000000, + box-shadow: inset -1px 0px 0px #000000, inset 1px 0px 0px #ffffff, inset 0px 1px 0px #ffffff, inset 0px 2px 0px #dfdfdf, @@ -71,7 +76,7 @@ $ui-highlight-color: $win95-window-header; src: url('../fonts/premillenium/MSSansSerif.ttf') format('truetype'); } -@import 'application'; +@import '../themes/glitch/styles/index'; // Imports glitch themes /* borrowed from cybrespace style: wider columns and full column width images */ @@ -174,7 +179,7 @@ body.admin { font-size:0px; color:$win95-bg; - background-image: url("../images/start.png"); + background-image: url("../images/start.png"); background-repeat:no-repeat; background-position:8%; background-clip:padding-box; @@ -336,7 +341,7 @@ body.admin { border-radius:0px; background-color:white; @include win95-border-inset(); - + width:12px; height:12px; } @@ -515,9 +520,9 @@ body.admin { color:black; font-weight:bold; } -.account__avatar, -.account__avatar-overlay-base, -.account__header__avatar, +.account__avatar, +.account__avatar-overlay-base, +.account__header__avatar, .account__avatar-overlay-overlay { @include win95-border-slight-inset(); clip-path:none; @@ -627,7 +632,7 @@ body.admin { } .status-card__description { - color:black; + color:black; } .account__display-name strong, .status__display-name strong { @@ -710,8 +715,8 @@ body.admin { width:40px; font-size:0px; color:$win95-bg; - - background-image: url("../images/start.png"); + + background-image: url("../images/start.png"); background-repeat:no-repeat; background-position:8%; background-clip:padding-box; @@ -723,7 +728,7 @@ body.admin { } .drawer__header a:first-child:hover { - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAOCAIAAACpTQvdAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAF3pUWHRBdXRob3IAAAiZS84oys9LzAEAC5oC6A7BY/IAAACWSURBVCiRhVJJDsQgDEuqOfRZ7a1P5gbP4uaJaEjTADMWQhHYjlk4p0wLnNdptdF4KvBUDyGzVwc2xO+uKtH+1o0ytEEmqFpuxlvFCGCxKbNIT56QCi2MzaA/2Mz+mERSOeqzJG2RUxkjdTabgPtFoZ1bZxcKvgPcLZVufAyR9Ni8v5dWDzfFx0giC1RvZFv6l35QQ/Mvv39XXgGzQpoAAAAASUVORK5CYII="); + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAOCAIAAACpTQvdAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAF3pUWHRBdXRob3IAAAiZS84oys9LzAEAC5oC6A7BY/IAAACWSURBVCiRhVJJDsQgDEuqOfRZ7a1P5gbP4uaJaEjTADMWQhHYjlk4p0wLnNdptdF4KvBUDyGzVwc2xO+uKtH+1o0ytEEmqFpuxlvFCGCxKbNIT56QCi2MzaA/2Mz+mERSOeqzJG2RUxkjdTabgPtFoZ1bZxcKvgPcLZVufAyR9Ni8v5dWDzfFx0giC1RvZFv6l35QQ/Mvv39XXgGzQpoAAAAASUVORK5CYII="); background-repeat:no-repeat; background-position:8%; background-clip:padding-box; @@ -732,7 +737,7 @@ body.admin { } .drawer__tab:first-child { - + } .search { @@ -844,7 +849,7 @@ body.admin { padding:4px 8px; } -.privacy-dropdown.active +.privacy-dropdown.active .privacy-dropdown__value { background: $win95-bg; box-shadow:unset; @@ -935,7 +940,7 @@ body.admin { background-color:$win95-bg; border:1px solid black; box-sizing:content-box; - + } .emoji-dialog .emoji-search { @@ -1010,8 +1015,8 @@ body.admin { width:60px; font-size:0px; color:$win95-bg; - - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAOCAIAAACpTQvdAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAF3pUWHRBdXRob3IAAAiZS84oys9LzAEAC5oC6A7BY/IAAACWSURBVCiRhVJJDsQgDEuqOfRZ7a1P5gbP4uaJaEjTADMWQhHYjlk4p0wLnNdptdF4KvBUDyGzVwc2xO+uKtH+1o0ytEEmqFpuxlvFCGCxKbNIT56QCi2MzaA/2Mz+mERSOeqzJG2RUxkjdTabgPtFoZ1bZxcKvgPcLZVufAyR9Ni8v5dWDzfFx0giC1RvZFv6l35QQ/Mvv39XXgGzQpoAAAAASUVORK5CYII="); + + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAOCAIAAACpTQvdAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAF3pUWHRBdXRob3IAAAiZS84oys9LzAEAC5oC6A7BY/IAAACWSURBVCiRhVJJDsQgDEuqOfRZ7a1P5gbP4uaJaEjTADMWQhHYjlk4p0wLnNdptdF4KvBUDyGzVwc2xO+uKtH+1o0ytEEmqFpuxlvFCGCxKbNIT56QCi2MzaA/2Mz+mERSOeqzJG2RUxkjdTabgPtFoZ1bZxcKvgPcLZVufAyR9Ni8v5dWDzfFx0giC1RvZFv6l35QQ/Mvv39XXgGzQpoAAAAASUVORK5CYII="); background-repeat:no-repeat; background-position:8%; background-clip:padding-box; @@ -1049,40 +1054,40 @@ body.admin { } } -.column-link[href="/web/timelines/public"] { - background-image: url("../images/icon_public.png"); +.column-link[href="/web/timelines/public"] { + background-image: url("../images/icon_public.png"); &:hover { background-image: url("../images/icon_public.png"); } } -.column-link[href="/web/timelines/public/local"] { - background-image: url("../images/icon_local.png"); +.column-link[href="/web/timelines/public/local"] { + background-image: url("../images/icon_local.png"); &:hover { background-image: url("../images/icon_local.png"); } } -.column-link[href="/web/pinned"] { - background-image: url("../images/icon_pin.png"); +.column-link[href="/web/pinned"] { + background-image: url("../images/icon_pin.png"); &:hover { background-image: url("../images/icon_pin.png"); } } -.column-link[href="/web/favourites"] { - background-image: url("../images/icon_likes.png"); +.column-link[href="/web/favourites"] { + background-image: url("../images/icon_likes.png"); &:hover { background-image: url("../images/icon_likes.png"); } } -.column-link[href="/web/blocks"] { - background-image: url("../images/icon_blocks.png"); +.column-link[href="/web/blocks"] { + background-image: url("../images/icon_blocks.png"); &:hover { background-image: url("../images/icon_blocks.png"); } } -.column-link[href="/web/mutes"] { - background-image: url("../images/icon_mutes.png"); +.column-link[href="/web/mutes"] { + background-image: url("../images/icon_mutes.png"); &:hover { background-image: url("../images/icon_mutes.png"); } } -.column-link[href="/settings/preferences"] { - background-image: url("../images/icon_settings.png"); +.column-link[href="/settings/preferences"] { + background-image: url("../images/icon_settings.png"); &:hover { background-image: url("../images/icon_settings.png"); } } -.column-link[href="/about/more"] { - background-image: url("../images/icon_about.png"); +.column-link[href="/about/more"] { + background-image: url("../images/icon_about.png"); &:hover { background-image: url("../images/icon_about.png"); } } -.column-link[href="/auth/sign_out"] { - background-image: url("../images/icon_logout.png"); +.column-link[href="/auth/sign_out"] { + background-image: url("../images/icon_logout.png"); &:hover { background-image: url("../images/icon_logout.png"); } } @@ -1098,7 +1103,7 @@ body.admin { line-height:30px; padding-left:20px; padding-right:40px; - + left:0px; bottom:-30px; display:block; @@ -1106,9 +1111,9 @@ body.admin { background-color:#7f7f7f; width:200%; height:30px; - + -ms-transform: rotate(-90deg); - + -webkit-transform: rotate(-90deg); transform: rotate(-90deg); transform-origin:top left; @@ -1189,7 +1194,7 @@ body.admin { left:unset; } -.dropdown > .icon-button, .detailed-status__button > .icon-button, +.dropdown > .icon-button, .detailed-status__button > .icon-button, .status__action-bar > .icon-button, .star-icon i { /* i don't know what's going on with the inline styles someone should look at the react code */ @@ -1239,8 +1244,8 @@ body.admin { background:$win95-bg; } -.actions-modal::before, -.boost-modal::before, +.actions-modal::before, +.boost-modal::before, .confirmation-modal::before, .report-modal::before { content: "Confirmation"; @@ -1278,8 +1283,8 @@ body.admin { .confirmation-modal__cancel-button { color:black; - &:active, - &:focus, + &:active, + &:focus, &:hover { color:black; } @@ -1566,10 +1571,10 @@ a.table-action-link:hover, background-color:white; } -.simple_form input[type=text], -.simple_form input[type=number], -.simple_form input[type=email], -.simple_form input[type=password], +.simple_form input[type=text], +.simple_form input[type=number], +.simple_form input[type=email], +.simple_form input[type=password], .simple_form textarea { color:black; background-color:white; @@ -1580,8 +1585,8 @@ a.table-action-link:hover, } } -.simple_form button, -.simple_form .button, +.simple_form button, +.simple_form .button, .simple_form .block-button { background: $win95-bg; @@ -1608,8 +1613,8 @@ a.table-action-link:hover, } } -.simple_form button.negative, -.simple_form .button.negative, +.simple_form button.negative, +.simple_form .button.negative, .simple_form .block-button.negative { background: $win95-bg; @@ -1631,8 +1636,8 @@ a.table-action-link:hover, border-right-color:#f5f5f5; width:12px; height:12px; - display:inline-block; - vertical-align:middle; + display:inline-block; + vertical-align:middle; margin-right:2px; } diff --git a/app/javascript/themes/glitch/containers/mastodon.js b/app/javascript/themes/glitch/containers/mastodon.js index 348470637..755b5564a 100644 --- a/app/javascript/themes/glitch/containers/mastodon.js +++ b/app/javascript/themes/glitch/containers/mastodon.js @@ -9,7 +9,7 @@ import UI from 'themes/glitch/features/ui'; import { hydrateStore } from 'themes/glitch/actions/store'; import { connectUserStream } from 'themes/glitch/actions/streaming'; import { IntlProvider, addLocaleData } from 'react-intl'; -import { getLocale } from 'mastodon/locales'; +import { getLocale } from 'locales'; import initialState from 'themes/glitch/util/initial_state'; const { localeData, messages } = getLocale(); diff --git a/app/javascript/themes/glitch/packs/common.js b/app/javascript/themes/glitch/packs/common.js index 3a62700bd..f4fa129e1 100644 --- a/app/javascript/themes/glitch/packs/common.js +++ b/app/javascript/themes/glitch/packs/common.js @@ -1,3 +1,3 @@ import 'font-awesome/css/font-awesome.css'; -require.context('../../images/', true); -import './styles/index.scss'; +require.context('images/', true); +import 'themes/glitch/styles/index.scss'; diff --git a/app/javascript/themes/glitch/packs/home.js b/app/javascript/themes/glitch/packs/home.js index dada28317..69dddf51c 100644 --- a/app/javascript/themes/glitch/packs/home.js +++ b/app/javascript/themes/glitch/packs/home.js @@ -1,7 +1,7 @@ -import loadPolyfills from './util/load_polyfills'; +import loadPolyfills from 'themes/glitch/util/load_polyfills'; loadPolyfills().then(() => { - require('./util/main').default(); + require('themes/glitch/util/main').default(); }).catch(e => { console.error(e); }); diff --git a/app/javascript/themes/glitch/packs/public.js b/app/javascript/themes/glitch/packs/public.js index 6adacad98..d9a1b9655 100644 --- a/app/javascript/themes/glitch/packs/public.js +++ b/app/javascript/themes/glitch/packs/public.js @@ -2,32 +2,14 @@ import loadPolyfills from 'themes/glitch/util/load_polyfills'; import { processBio } from 'themes/glitch/util/bio_metadata'; import ready from 'themes/glitch/util/ready'; -window.addEventListener('message', e => { - const data = e.data || {}; - - if (!window.parent || data.type !== 'setHeight') { - return; - } - - ready(() => { - window.parent.postMessage({ - type: 'setHeight', - id: data.id, - height: document.getElementsByTagName('html')[0].scrollHeight, - }, '*'); - }); -}); - function main() { - const { length } = require('stringz'); const IntlRelativeFormat = require('intl-relativeformat').default; - const { delegate } = require('rails-ujs'); - const emojify = require('../themes/glitch/util/emoji').default; - const { getLocale } = require('mastodon/locales'); + const emojify = require('themes/glitch/util/emoji').default; + const { getLocale } = require('locales'); const { localeData } = getLocale(); - const VideoContainer = require('../themes/glitch/containers/video_container').default; - const MediaGalleryContainer = require('../themes/glitch/containers/media_gallery_container').default; - const CardContainer = require('../themes/glitch/containers/card_container').default; + const VideoContainer = require('themes/glitch/containers/video_container').default; + const MediaGalleryContainer = require('themes/glitch/containers/media_gallery_container').default; + const CardContainer = require('themes/glitch/containers/card_container').default; const React = require('react'); const ReactDOM = require('react-dom'); @@ -87,61 +69,6 @@ function main() { ReactDOM.render(, content); }); }); - - delegate(document, '.webapp-btn', 'click', ({ target, button }) => { - if (button !== 0) { - return true; - } - window.location.href = target.href; - return false; - }); - - delegate(document, '.status__content__spoiler-link', 'click', ({ target }) => { - const contentEl = target.parentNode.parentNode.querySelector('.e-content'); - - if (contentEl.style.display === 'block') { - contentEl.style.display = 'none'; - target.parentNode.style.marginBottom = 0; - } else { - contentEl.style.display = 'block'; - target.parentNode.style.marginBottom = null; - } - - return false; - }); - - delegate(document, '.account_display_name', 'input', ({ target }) => { - const nameCounter = document.querySelector('.name-counter'); - - if (nameCounter) { - nameCounter.textContent = 30 - length(target.value); - } - }); - - delegate(document, '.account_note', 'input', ({ target }) => { - const noteCounter = document.querySelector('.note-counter'); - - if (noteCounter) { - const noteWithoutMetadata = processBio(target.value).text; - noteCounter.textContent = 500 - length(noteWithoutMetadata); - } - }); - - delegate(document, '#account_avatar', 'change', ({ target }) => { - const avatar = document.querySelector('.card.compact .avatar img'); - const [file] = target.files || []; - const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc; - - avatar.src = url; - }); - - delegate(document, '#account_header', 'change', ({ target }) => { - const header = document.querySelector('.card.compact'); - const [file] = target.files || []; - const url = file ? URL.createObjectURL(file) : header.dataset.originalSrc; - - header.style.backgroundImage = `url(${url})`; - }); } loadPolyfills().then(main).catch(error => { diff --git a/app/javascript/themes/glitch/theme.yml b/app/javascript/themes/glitch/theme.yml index cf3fa32c2..ac6f57546 100644 --- a/app/javascript/themes/glitch/theme.yml +++ b/app/javascript/themes/glitch/theme.yml @@ -1,25 +1,34 @@ # (REQUIRED) The location of the pack files. pack: about: packs/about.js - admin: null - common: packs/common.js - embed: null - home: packs/home.js + admin: + auth: + common: + filename: packs/common.js + stylesheet: true + embed: packs/public.js + error: + home: + filename: packs/home.js + preload: + - themes/glitch/async/getting_started + - themes/glitch/async/compose + - themes/glitch/async/home_timeline + - themes/glitch/async/notifications + stylesheet: true + modal: public: packs/public.js - settings: null + settings: share: packs/share.js # (OPTIONAL) The directory which contains the pack files. # Defaults to the theme directory (`app/javascript/themes/[theme]`), # which should be sufficient for like 99% of use-cases lol. -# pack_directory: app/javascript/packs -# (OPTIONAL) Additional javascript resources to preload, for use with -# lazy-loaded components. It is **STRONGLY RECOMMENDED** that you -# derive these pathnames from `themes/[your-theme]` to ensure that -# they stay unique. -preload: -- themes/glitch/async/getting_started -- themes/glitch/async/compose -- themes/glitch/async/home_timeline -- themes/glitch/async/notifications +# pack_directory: app/javascript/packs + +# (OPTIONAL) By default the theme will fallback to the default theme +# if a particular pack is not provided. You can specify different +# fallbacks here, or disable fallback behaviours altogether by +# specifying a `null` value. +fallback: diff --git a/app/javascript/themes/vanilla/theme.yml b/app/javascript/themes/vanilla/theme.yml index b4a1598fc..67fd9723e 100644 --- a/app/javascript/themes/vanilla/theme.yml +++ b/app/javascript/themes/vanilla/theme.yml @@ -1,12 +1,23 @@ # (REQUIRED) The location of the pack files inside `pack_directory`. pack: about: about.js - admin: null - common: common.js - embed: null - home: application.js + admin: + auth: + common: + filename: common.js + stylesheet: true + embed: public.js + error: + home: + filename: application.js + preload: + - features/getting_started + - features/compose + - features/home_timeline + - features/notifications + modal: public: public.js - settings: null + settings: share: share.js # (OPTIONAL) The directory which contains the pack files. @@ -15,12 +26,8 @@ pack: # somewhere else. pack_directory: app/javascript/packs -# (OPTIONAL) Additional javascript resources to preload, for use with -# lazy-loaded components. It is **STRONGLY RECOMMENDED** that you -# derive these pathnames from `themes/[your-theme]` to ensure that -# they stay unique. (Of course, vanilla doesn't do this ^^;;) -preload: -- features/getting_started -- features/compose -- features/home_timeline -- features/notifications +# (OPTIONAL) By default the theme will fallback to the default theme +# if a particular pack is not provided. You can specify different +# fallbacks here, or disable fallback behaviours altogether by +# specifying a `null` value. +fallback: diff --git a/app/javascript/themes/win95/index.js b/app/javascript/themes/win95/index.js new file mode 100644 index 000000000..bed6a1ef3 --- /dev/null +++ b/app/javascript/themes/win95/index.js @@ -0,0 +1,10 @@ +// These lines are the same as in glitch: +import 'font-awesome/css/font-awesome.css'; +require.context('../../images/', true); + +// …But we want to use our own styles instead. +import 'styles/win95.scss'; + +// Be sure to make this style file import from +// `themes/glitch/styles/index.scss` (the glitch styling), and not +// `application.scss` (which are the vanilla styles). diff --git a/app/javascript/themes/win95/theme.yml b/app/javascript/themes/win95/theme.yml new file mode 100644 index 000000000..43af38198 --- /dev/null +++ b/app/javascript/themes/win95/theme.yml @@ -0,0 +1,23 @@ +# win95 theme. + +# Ported over from `cybrespace:mastodon/theme_win95`. +# + +# You can use this theme file as inspiration for porting over +# a preëxisting Mastodon theme. + +# We only modify the `common` pack, which contains our styling. +pack: + common: + filename: index.js + stylesheet: true + # All unspecified packs will inherit from glitch. + +# By default, the glitch preloads will also be used here. You can +# disable them by setting `preload` to `null`. + +# preload: + +# The `fallback` parameter tells us to use glitch files for everything +# we haven't specified. +fallback: glitch diff --git a/app/lib/themes.rb b/app/lib/themes.rb index f7ec22fd2..7ced9f945 100644 --- a/app/lib/themes.rb +++ b/app/lib/themes.rb @@ -7,15 +7,27 @@ class Themes include Singleton def initialize + + core = YAML.load_file(Rails.root.join('app', 'javascript', 'core', 'theme.yml')) + core['pack'] = Hash.new unless core['pack'] + result = Hash.new Dir.glob(Rails.root.join('app', 'javascript', 'themes', '*', 'theme.yml')) do |path| data = YAML.load_file(path) name = File.basename(File.dirname(path)) if data['pack'] + data['name'] = name result[name] = data end end + + @core = core @conf = result + + end + + def core + @core end def get(name) diff --git a/app/views/about/more.html.haml b/app/views/about/more.html.haml index 7ffa5ecc3..d92362bd7 100644 --- a/app/views/about/more.html.haml +++ b/app/views/about/more.html.haml @@ -2,7 +2,6 @@ = site_hostname - content_for :header_tags do - = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous' = render partial: 'shared/og' .landing-page diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml index 385b0b1dc..4f5b53470 100644 --- a/app/views/about/show.html.haml +++ b/app/views/about/show.html.haml @@ -3,7 +3,6 @@ - content_for :header_tags do %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json) - = javascript_pack_tag 'about', integrity: true, crossorigin: 'anonymous' = render partial: 'shared/og' .landing-page diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml index 5747cc274..7dd962bf2 100644 --- a/app/views/admin/reports/show.html.haml +++ b/app/views/admin/reports/show.html.haml @@ -1,6 +1,3 @@ -- content_for :header_tags do - = javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous' - - content_for :page_title do = t('admin.reports.report', id: @report.id) diff --git a/app/views/admin/statuses/index.html.haml b/app/views/admin/statuses/index.html.haml index fe2581527..9747a92cf 100644 --- a/app/views/admin/statuses/index.html.haml +++ b/app/views/admin/statuses/index.html.haml @@ -1,6 +1,3 @@ -- content_for :header_tags do - = javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous' - - content_for :page_title do = t('admin.statuses.title') diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 63b3a0c26..e8a81656c 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -1,13 +1,7 @@ - content_for :header_tags do - - if theme_data['preload'] - - theme_data['preload'].each do |link| - %link{ href: asset_pack_path("#{link}.js"), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/ %meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key} %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json) - = javascript_pack_tag "themes/#{current_theme}", integrity: true, crossorigin: 'anonymous' - = stylesheet_pack_tag "themes/#{current_theme}", integrity: true, media: 'all' - .app-holder#mastodon{ data: { props: Oj.dump(default_props) } } %noscript = image_tag asset_pack_path('logo.svg'), alt: 'Mastodon' diff --git a/app/views/layouts/_theme.html.haml b/app/views/layouts/_theme.html.haml new file mode 100644 index 000000000..cdec4b370 --- /dev/null +++ b/app/views/layouts/_theme.html.haml @@ -0,0 +1,10 @@ +- if theme + - if theme[:pack] != 'common' && theme[:common] + = render partial: 'layouts/theme', object: theme[:common] + - if theme[:pack] + = javascript_pack_tag theme[:name] ? "themes/#{theme[:name]}/#{theme[:pack]}" : "core/#{theme[:pack]}", integrity: true, crossorigin: 'anonymous' + - if theme[:stylesheet] + = stylesheet_pack_tag theme[:name] ? "themes/#{theme[:name]}/#{theme[:pack]}" : "core/#{theme[:pack]}", integrity: true, media: 'all' + - if theme[:preload] + - theme[:preload].each do |link| + %link{ href: asset_pack_path("#{link}.js"), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/ diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml index c98d85f7b..66382db50 100644 --- a/app/views/layouts/admin.html.haml +++ b/app/views/layouts/admin.html.haml @@ -1,6 +1,3 @@ -- content_for :header_tags do - = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous' - - content_for :content do .admin-wrapper .sidebar-wrapper diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 24b74c787..99ae7d90d 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -18,16 +18,16 @@ = ' - ' = title - = stylesheet_pack_tag 'common', media: 'all' - = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous' + = javascript_pack_tag "locales", integrity: true, crossorigin: 'anonymous' = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous' = csrf_meta_tags - - if controller_name != 'home' - = stylesheet_pack_tag 'application', integrity: true, media: 'all' - = yield :header_tags + -# These must come after :header_tags to ensure our initial state has been defined. + = render partial: 'layouts/theme', object: @core + = render partial: 'layouts/theme', object: @theme + - body_classes ||= @body_classes || '' - body_classes += ' system-font' if current_account&.user&.setting_system_font_ui diff --git a/app/views/layouts/auth.html.haml b/app/views/layouts/auth.html.haml index d8ac733f9..f4812ac6a 100644 --- a/app/views/layouts/auth.html.haml +++ b/app/views/layouts/auth.html.haml @@ -1,6 +1,3 @@ -- content_for :header_tags do - = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous' - - content_for :content do .container .logo-container diff --git a/app/views/layouts/embedded.html.haml b/app/views/layouts/embedded.html.haml index 5fc60be17..3960167bf 100644 --- a/app/views/layouts/embedded.html.haml +++ b/app/views/layouts/embedded.html.haml @@ -4,10 +4,9 @@ %meta{ charset: 'utf-8' }/ %meta{ name: 'robots', content: 'noindex' }/ - = stylesheet_pack_tag 'common', media: 'all' = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous' - = stylesheet_pack_tag 'application', integrity: true, media: 'all' + = javascript_pack_tag 'embed', integrity: true, crossorigin: 'anonymous' = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous' - = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous' - %body.embed + = render partial: 'layouts/theme', object: @core + = render partial: 'layouts/theme', object: @theme = yield diff --git a/app/views/layouts/error.html.haml b/app/views/layouts/error.html.haml index d0eae4434..9904b8fdd 100644 --- a/app/views/layouts/error.html.haml +++ b/app/views/layouts/error.html.haml @@ -5,8 +5,8 @@ %meta{ charset: 'utf-8' }/ %title= safe_join([yield(:page_title), Setting.default_settings['site_title']], ' - ') %meta{ content: 'width=device-width,initial-scale=1', name: 'viewport' }/ - = stylesheet_pack_tag 'common', media: 'all' - = stylesheet_pack_tag 'application', integrity: true, media: 'all' + = render partial: 'layouts/theme', object: @core + = render partial: 'layouts/theme', object: @theme %body.error .dialog %img{ alt: Setting.default_settings['site_title'], src: '/oops.gif' }/ diff --git a/app/views/layouts/modal.html.haml b/app/views/layouts/modal.html.haml index a819e098d..d3519f032 100644 --- a/app/views/layouts/modal.html.haml +++ b/app/views/layouts/modal.html.haml @@ -1,6 +1,3 @@ -- content_for :header_tags do - = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous' - - content_for :content do - if user_signed_in? .account-header diff --git a/app/views/layouts/public.html.haml b/app/views/layouts/public.html.haml index 83e92b938..b3795eaad 100644 --- a/app/views/layouts/public.html.haml +++ b/app/views/layouts/public.html.haml @@ -1,6 +1,3 @@ -- content_for :header_tags do - = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous' - - content_for :content do .container= yield .footer diff --git a/app/views/shares/show.html.haml b/app/views/shares/show.html.haml index 44b6f145f..4c0390c42 100644 --- a/app/views/shares/show.html.haml +++ b/app/views/shares/show.html.haml @@ -1,5 +1,4 @@ - content_for :header_tags do %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json) - = javascript_pack_tag 'share', integrity: true, crossorigin: 'anonymous' #mastodon-compose{ data: { props: Oj.dump(default_props) } } diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml index ea8b0faa3..e05fe1c39 100644 --- a/app/views/tags/show.html.haml +++ b/app/views/tags/show.html.haml @@ -3,7 +3,6 @@ - content_for :header_tags do %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json) - = javascript_pack_tag 'about', integrity: true, crossorigin: 'anonymous' = render 'og' .landing-page.tag-page diff --git a/config/webpack/configuration.js b/config/webpack/configuration.js index 74f75d89b..9514bc547 100644 --- a/config/webpack/configuration.js +++ b/config/webpack/configuration.js @@ -12,14 +12,24 @@ const settings = safeLoad(readFileSync(configPath), 'utf8')[env.NODE_ENV]; const themeFiles = glob.sync('app/javascript/themes/*/theme.yml'); const themes = {}; +const core = function () { + const coreFile = resolve('app', 'javascript', 'core', 'theme.yml'); + const data = safeLoad(readFileSync(coreFile), 'utf8'); + if (!data.pack_directory) { + data.pack_directory = dirname(coreFile); + } + return data.pack ? data : {}; +}(); + for (let i = 0; i < themeFiles.length; i++) { const themeFile = themeFiles[i]; const data = safeLoad(readFileSync(themeFile), 'utf8'); + data.name = basename(dirname(themeFile)); if (!data.pack_directory) { data.pack_directory = dirname(themeFile); } if (data.pack) { - themes[basename(dirname(themeFile))] = data; + themes[data.name] = data; } } @@ -43,6 +53,7 @@ const output = { module.exports = { settings, + core, themes, env, loadersDir, diff --git a/config/webpack/generateLocalePacks.js b/config/webpack/generateLocalePacks.js index cd3bed50c..a943589f7 100644 --- a/config/webpack/generateLocalePacks.js +++ b/config/webpack/generateLocalePacks.js @@ -57,7 +57,7 @@ Object.keys(glitchMessages).forEach(function (key) { // import messages from '../../app/javascript/mastodon/locales/${locale}.json'; import localeData from ${JSON.stringify(localeDataPath)}; -import { setLocale } from '../../app/javascript/mastodon/locales'; +import { setLocale } from 'locales'; ${glitchInject} setLocale({messages: mergedMessages, localeData: localeData}); `; diff --git a/config/webpack/shared.js b/config/webpack/shared.js index 5d176db4e..5b90f27fb 100644 --- a/config/webpack/shared.js +++ b/config/webpack/shared.js @@ -6,33 +6,37 @@ const { sync } = require('glob'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); const ManifestPlugin = require('webpack-manifest-plugin'); const extname = require('path-complete-extname'); -const { env, settings, themes, output, loadersDir } = require('./configuration.js'); +const { env, settings, core, themes, output, loadersDir } = require('./configuration.js'); const localePackPaths = require('./generateLocalePacks'); -const extensionGlob = `**/*{${settings.extensions.join(',')}}*`; -const entryPath = join(settings.source_path, settings.source_entry_path); -const packPaths = sync(join(entryPath, extensionGlob)); +function reducePacks (data, into = {}) { + if (!data.pack) { + return into; + } + Object.keys(data.pack).reduce((map, entry) => { + const pack = data.pack[entry]; + if (!pack) { + return map; + } + const packFile = typeof pack === 'string' ? pack : pack.filename; + if (packFile) { + map[data.name ? `themes/${data.name}/${entry}` : `core/${entry}`] = resolve(data.pack_directory, packFile); + } + return map; + }, into); + return into; +} module.exports = { entry: Object.assign( - packPaths.reduce((map, entry) => { - const localMap = map; - const namespace = relative(join(entryPath), dirname(entry)); - localMap[join(namespace, basename(entry, extname(entry)))] = resolve(entry); - return localMap; - }, {}), + { locales: resolve('app', 'javascript', 'locales') }, localePackPaths.reduce((map, entry) => { const localMap = map; localMap[basename(entry, extname(entry, extname(entry)))] = resolve(entry); return localMap; }, {}), - Object.keys(themes).reduce( - (themePaths, name) => { - const themeData = themes[name]; - themePaths[`themes/${name}`] = resolve(themeData.pack_directory, themeData.pack); - return themePaths; - }, {} - ) + reducePacks(core), + Object.keys(themes).reduce((map, entry) => reducePacks(themes[entry], map), {}) ), output: { @@ -64,7 +68,7 @@ module.exports = { writeToFileEmit: true, }), new webpack.optimize.CommonsChunkPlugin({ - name: 'common', + name: 'locales', minChunks: Infinity, // It doesn't make sense to use common chunks with multiple frontend support. }), ], diff --git a/config/webpacker.yml b/config/webpacker.yml index 8d8470651..50d95813a 100644 --- a/config/webpacker.yml +++ b/config/webpacker.yml @@ -2,7 +2,6 @@ default: &default source_path: app/javascript - source_entry_path: packs public_output_path: packs cache_path: tmp/cache/webpacker @@ -13,17 +12,6 @@ default: &default # Reload manifest.json on all requests so we reload latest compiled packs cache_manifest: false - extensions: - - .js - - .sass - - .scss - - .css - - .png - - .svg - - .gif - - .jpeg - - .jpg - development: <<: *default compile: true -- cgit From 541fe9b110fce15c42ba15df27926552c234afd0 Mon Sep 17 00:00:00 2001 From: kibigo! Date: Thu, 30 Nov 2017 19:29:47 -0800 Subject: Skins support --- app/controllers/application_controller.rb | 43 ++++++++++++------- app/controllers/settings/preferences_controller.rb | 1 + app/javascript/core/common.js | 3 ++ app/javascript/core/settings.js | 6 +++ app/javascript/core/theme.yml | 4 +- app/javascript/packs/common.js | 4 +- app/javascript/skins/vanilla/win95.scss | 1 + app/javascript/styles/mastodon/components.scss | 8 ++-- app/javascript/styles/win95.scss | 48 +++++++++++----------- app/javascript/themes/glitch/packs/common.js | 2 - app/javascript/themes/glitch/packs/public.js | 1 - app/javascript/themes/win95/index.js | 10 ----- app/javascript/themes/win95/theme.yml | 18 -------- app/lib/themes.rb | 25 +++++++++++ app/lib/user_settings_decorator.rb | 7 +++- app/models/user.rb | 2 +- app/views/layouts/_theme.html.haml | 7 +++- app/views/settings/preferences/show.html.haml | 1 + config/locales/simple_form.en.yml | 2 + config/settings.yml | 1 + config/webpack/configuration.js | 28 +++++++++++-- config/webpack/shared.js | 20 ++++++++- 22 files changed, 157 insertions(+), 85 deletions(-) create mode 100644 app/javascript/skins/vanilla/win95.scss delete mode 100644 app/javascript/themes/win95/index.js delete mode 100644 app/javascript/themes/win95/theme.yml (limited to 'app/javascript/core') diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 7cc4eea27..f5753963d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -12,6 +12,8 @@ class ApplicationController < ActionController::Base helper_method :current_account helper_method :current_session + helper_method :current_theme + helper_method :current_skin helper_method :single_user_mode? rescue_from ActionController::RoutingError, with: :not_found @@ -52,14 +54,14 @@ class ApplicationController < ActionController::Base new_user_session_path end - def pack(data, pack_name) + def pack(data, pack_name, skin = 'default') return nil unless pack?(data, pack_name) pack_data = { common: pack_name == 'common' ? nil : resolve_pack(data['name'] ? Themes.instance.get(current_theme) : Themes.instance.core, 'common'), name: data['name'], pack: pack_name, preload: nil, - stylesheet: false + skin: nil, } if data['pack'][pack_name].is_a?(Hash) pack_data[:common] = nil if data['pack'][pack_name]['use_common'] == false @@ -68,7 +70,11 @@ class ApplicationController < ActionController::Base pack_data[:preload] = [data['pack'][pack_name]['preload']] if data['pack'][pack_name]['preload'].is_a?(String) pack_data[:preload] = data['pack'][pack_name]['preload'] if data['pack'][pack_name]['preload'].is_a?(Array) end - pack_data[:stylesheet] = true if data['pack'][pack_name]['stylesheet'] + if skin != 'default' && data['skin'][skin] + pack_data[:skin] = skin if data['skin'][skin].include?(pack_name) + else # default skin + pack_data[:skin] = 'default' if data['pack'][pack_name]['stylesheet'] + end end pack_data end @@ -80,39 +86,39 @@ class ApplicationController < ActionController::Base false end - def nil_pack(data, pack_name) + def nil_pack(data, pack_name, skin = 'default') { - common: pack_name == 'common' ? nil : resolve_pack(data['name'] ? Themes.instance.get(current_theme) : Themes.instance.core, 'common'), + common: pack_name == 'common' ? nil : resolve_pack(data['name'] ? Themes.instance.get(current_theme) : Themes.instance.core, 'common', skin), name: data['name'], pack: nil, preload: nil, - stylesheet: false + skin: nil, } end - def resolve_pack(data, pack_name) - result = pack(data, pack_name) + def resolve_pack(data, pack_name, skin = 'default') + result = pack(data, pack_name, skin) unless result if data['name'] && data.key?('fallback') if data['fallback'].nil? - return nil_pack(data, pack_name) + return nil_pack(data, pack_name, skin) elsif data['fallback'].is_a?(String) && Themes.instance.get(data['fallback']) - return resolve_pack(Themes.instance.get(data['fallback']), pack_name) + return resolve_pack(Themes.instance.get(data['fallback']), pack_name, skin) elsif data['fallback'].is_a?(Array) data['fallback'].each do |fallback| - return resolve_pack(Themes.instance.get(fallback), pack_name) if Themes.instance.get(fallback) + return resolve_pack(Themes.instance.get(fallback), pack_name, skin) if Themes.instance.get(fallback) end end - return nil_pack(data, pack_name) + return nil_pack(data, pack_name, skin) end - return data.key?('name') && data['name'] != default_theme ? resolve_pack(Themes.instance.get(default_theme), pack_name) : nil_pack(data, pack_name) + return data.key?('name') && data['name'] != default_theme ? resolve_pack(Themes.instance.get(default_theme), pack_name, skin) : nil_pack(data, pack_name, skin) end result end def use_pack(pack_name) @core = resolve_pack(Themes.instance.core, pack_name) - @theme = resolve_pack(Themes.instance.get(current_theme), pack_name) + @theme = resolve_pack(Themes.instance.get(current_theme), pack_name, current_skin) end protected @@ -154,6 +160,15 @@ class ApplicationController < ActionController::Base current_user.setting_theme end + def default_skin + 'default' + end + + def current_skin + return default_skin unless Themes.instance.skins_for(current_theme).include? current_user&.setting_skin + current_user.setting_skin + end + def cache_collection(raw, klass) return raw unless klass.respond_to?(:with_includes) diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 3aefd90a2..56baebed2 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -39,6 +39,7 @@ class Settings::PreferencesController < Settings::BaseController :setting_system_font_ui, :setting_noindex, :setting_theme, + :setting_skin, notification_emails: %i(follow follow_request reblog favourite mention digest), interactions: %i(must_be_follower must_be_following) ) diff --git a/app/javascript/core/common.js b/app/javascript/core/common.js index 24c0fdf61..bb4b97935 100644 --- a/app/javascript/core/common.js +++ b/app/javascript/core/common.js @@ -1,5 +1,8 @@ // This file will be loaded on all pages, regardless of theme. import { start } from 'rails-ujs'; +import 'font-awesome/css/font-awesome.css'; + +require.context('images/', true); start(); diff --git a/app/javascript/core/settings.js b/app/javascript/core/settings.js index 7fb1d8e77..bc5f9ed1d 100644 --- a/app/javascript/core/settings.js +++ b/app/javascript/core/settings.js @@ -3,6 +3,8 @@ const { length } = require('stringz'); const { delegate } = require('rails-ujs'); +import { processBio } from 'themes/glitch/util/bio_metadata'; + delegate(document, '.account_display_name', 'input', ({ target }) => { const nameCounter = document.querySelector('.name-counter'); @@ -35,3 +37,7 @@ delegate(document, '#account_header', 'change', ({ target }) => { header.style.backgroundImage = `url(${url})`; }); + +delegate(document, '#user_setting_theme', 'change', ({ target }) => { + target.form.submit(); +}); diff --git a/app/javascript/core/theme.yml b/app/javascript/core/theme.yml index 17e8e66b3..0dc05a149 100644 --- a/app/javascript/core/theme.yml +++ b/app/javascript/core/theme.yml @@ -4,7 +4,9 @@ pack: about: admin: admin.js auth: - common: common.js + common: + filename: common.js + stylesheet: true embed: embed.js error: home: diff --git a/app/javascript/packs/common.js b/app/javascript/packs/common.js index f3156c1c6..5d42509c5 100644 --- a/app/javascript/packs/common.js +++ b/app/javascript/packs/common.js @@ -1,3 +1 @@ -import 'font-awesome/css/font-awesome.css'; -import 'styles/application.scss' -require.context('../images/', true); +import 'styles/application.scss'; diff --git a/app/javascript/skins/vanilla/win95.scss b/app/javascript/skins/vanilla/win95.scss new file mode 100644 index 000000000..298f6ee9d --- /dev/null +++ b/app/javascript/skins/vanilla/win95.scss @@ -0,0 +1 @@ +@import 'styles/win95'; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 0ded6f159..8566585c5 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2075,7 +2075,7 @@ .getting-started { box-sizing: border-box; padding-bottom: 235px; - background: url('../images/mastodon-getting-started.png') no-repeat 0 100%; + background: url('~images/mastodon-getting-started.png') no-repeat 0 100%; flex: 1 0 auto; p { @@ -2270,7 +2270,7 @@ button.icon-button.active i.fa-retweet { justify-content: center; & > div { - background: url('../images/mastodon-not-found.png') no-repeat center -50px; + background: url('~images/mastodon-not-found.png') no-repeat center -50px; padding-top: 210px; width: 100%; } @@ -3143,7 +3143,7 @@ button.icon-button.active i.fa-retweet { img, canvas { display: block; - background: url('../images/void.png') repeat; + background: url('~images/void.png') repeat; object-fit: contain; } @@ -3390,7 +3390,7 @@ button.icon-button.active i.fa-retweet { } .onboarding-modal__page-one__elephant-friend { - background: url('../images/elephant-friend-1.png') no-repeat center center / contain; + background: url('~images/elephant-friend-1.png') no-repeat center center / contain; width: 155px; height: 193px; margin-right: 15px; diff --git a/app/javascript/styles/win95.scss b/app/javascript/styles/win95.scss index 6c89fc5bf..d683218b0 100644 --- a/app/javascript/styles/win95.scss +++ b/app/javascript/styles/win95.scss @@ -1,7 +1,7 @@ // win95 theme from cybrespace. -// Modified to inherit glitch styles (themes/glitch/styles/index.scss) -// instead of vanilla ones (./application.scss) +// Modified by kibi! to use webpack package syntax for urls (eg, +// `url(~images/…)`) for easy importing into skins. $win95-bg: #bfbfbf; $win95-dark-grey: #404040; @@ -73,10 +73,10 @@ $ui-highlight-color: $win95-window-header; @font-face { font-family:"premillenium"; - src: url('../fonts/premillenium/MSSansSerif.ttf') format('truetype'); + src: url('~fonts/premillenium/MSSansSerif.ttf') format('truetype'); } -@import '../themes/glitch/styles/index'; // Imports glitch themes +@import 'application'; /* borrowed from cybrespace style: wider columns and full column width images */ @@ -179,7 +179,7 @@ body.admin { font-size:0px; color:$win95-bg; - background-image: url("../images/start.png"); + background-image: url("~images/start.png"); background-repeat:no-repeat; background-position:8%; background-clip:padding-box; @@ -716,7 +716,7 @@ body.admin { font-size:0px; color:$win95-bg; - background-image: url("../images/start.png"); + background-image: url("~images/start.png"); background-repeat:no-repeat; background-position:8%; background-clip:padding-box; @@ -1055,40 +1055,40 @@ body.admin { } .column-link[href="/web/timelines/public"] { - background-image: url("../images/icon_public.png"); - &:hover { background-image: url("../images/icon_public.png"); } + background-image: url("~images/icon_public.png"); + &:hover { background-image: url("~images/icon_public.png"); } } .column-link[href="/web/timelines/public/local"] { - background-image: url("../images/icon_local.png"); - &:hover { background-image: url("../images/icon_local.png"); } + background-image: url("~images/icon_local.png"); + &:hover { background-image: url("~images/icon_local.png"); } } .column-link[href="/web/pinned"] { - background-image: url("../images/icon_pin.png"); - &:hover { background-image: url("../images/icon_pin.png"); } + background-image: url("~images/icon_pin.png"); + &:hover { background-image: url("~images/icon_pin.png"); } } .column-link[href="/web/favourites"] { - background-image: url("../images/icon_likes.png"); - &:hover { background-image: url("../images/icon_likes.png"); } + background-image: url("~images/icon_likes.png"); + &:hover { background-image: url("~images/icon_likes.png"); } } .column-link[href="/web/blocks"] { - background-image: url("../images/icon_blocks.png"); - &:hover { background-image: url("../images/icon_blocks.png"); } + background-image: url("~images/icon_blocks.png"); + &:hover { background-image: url("~images/icon_blocks.png"); } } .column-link[href="/web/mutes"] { - background-image: url("../images/icon_mutes.png"); - &:hover { background-image: url("../images/icon_mutes.png"); } + background-image: url("~images/icon_mutes.png"); + &:hover { background-image: url("~images/icon_mutes.png"); } } .column-link[href="/settings/preferences"] { - background-image: url("../images/icon_settings.png"); - &:hover { background-image: url("../images/icon_settings.png"); } + background-image: url("~images/icon_settings.png"); + &:hover { background-image: url("~images/icon_settings.png"); } } .column-link[href="/about/more"] { - background-image: url("../images/icon_about.png"); - &:hover { background-image: url("../images/icon_about.png"); } + background-image: url("~images/icon_about.png"); + &:hover { background-image: url("~images/icon_about.png"); } } .column-link[href="/auth/sign_out"] { - background-image: url("../images/icon_logout.png"); - &:hover { background-image: url("../images/icon_logout.png"); } + background-image: url("~images/icon_logout.png"); + &:hover { background-image: url("~images/icon_logout.png"); } } .getting-started__footer { diff --git a/app/javascript/themes/glitch/packs/common.js b/app/javascript/themes/glitch/packs/common.js index f4fa129e1..fd4254a0e 100644 --- a/app/javascript/themes/glitch/packs/common.js +++ b/app/javascript/themes/glitch/packs/common.js @@ -1,3 +1 @@ -import 'font-awesome/css/font-awesome.css'; -require.context('images/', true); import 'themes/glitch/styles/index.scss'; diff --git a/app/javascript/themes/glitch/packs/public.js b/app/javascript/themes/glitch/packs/public.js index d9a1b9655..410e66716 100644 --- a/app/javascript/themes/glitch/packs/public.js +++ b/app/javascript/themes/glitch/packs/public.js @@ -1,5 +1,4 @@ import loadPolyfills from 'themes/glitch/util/load_polyfills'; -import { processBio } from 'themes/glitch/util/bio_metadata'; import ready from 'themes/glitch/util/ready'; function main() { diff --git a/app/javascript/themes/win95/index.js b/app/javascript/themes/win95/index.js deleted file mode 100644 index bed6a1ef3..000000000 --- a/app/javascript/themes/win95/index.js +++ /dev/null @@ -1,10 +0,0 @@ -// These lines are the same as in glitch: -import 'font-awesome/css/font-awesome.css'; -require.context('../../images/', true); - -// …But we want to use our own styles instead. -import 'styles/win95.scss'; - -// Be sure to make this style file import from -// `themes/glitch/styles/index.scss` (the glitch styling), and not -// `application.scss` (which are the vanilla styles). diff --git a/app/javascript/themes/win95/theme.yml b/app/javascript/themes/win95/theme.yml deleted file mode 100644 index c4ac8aa55..000000000 --- a/app/javascript/themes/win95/theme.yml +++ /dev/null @@ -1,18 +0,0 @@ -# win95 theme. - -# Ported over from `cybrespace:mastodon/theme_win95`. -# - -# You can use this theme file as inspiration for porting over -# a preëxisting Mastodon theme. - -# We only modify the `common` pack, which contains our styling. -pack: - common: - filename: index.js - stylesheet: true - # All unspecified packs will inherit from glitch. - -# The `fallback` parameter tells us to use glitch files for everything -# we haven't specified. -fallback: glitch diff --git a/app/lib/themes.rb b/app/lib/themes.rb index 7ced9f945..0819e2c90 100644 --- a/app/lib/themes.rb +++ b/app/lib/themes.rb @@ -17,10 +17,31 @@ class Themes name = File.basename(File.dirname(path)) if data['pack'] data['name'] = name + data['skin'] = { 'default' => [] } result[name] = data end end + Dir.glob(Rails.root.join('app', 'javascript', 'skins', '*', '*')) do |path| + ext = File.extname(path) + skin = File.basename(path) + name = File.basename(File.dirname(path)) + if result[name] + if File.directory?(path) + pack = [] + Dir.glob(File.join(path, '*.{css,scss}')) do |sheet| + pack.push(File.basename(sheet, File.extname(sheet))) + end + elsif ext.match?(/^\.s?css$/i) + skin = File.basename(path, ext) + pack = ['common'] + end + if skin != 'default' + result[name]['skin'][skin] = pack + end + end + end + @core = core @conf = result @@ -37,4 +58,8 @@ class Themes def names @conf.keys end + + def skins_for(name) + @conf[name]['skin'].keys + end end diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index d86959c0b..730c70177 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -27,6 +27,7 @@ class UserSettingsDecorator user.settings['system_font_ui'] = system_font_ui_preference if change?('setting_system_font_ui') user.settings['noindex'] = noindex_preference if change?('setting_noindex') user.settings['theme'] = theme_preference if change?('setting_theme') + user.settings['skin'] = skin_preference if change?('setting_skin') end def merged_notification_emails @@ -76,7 +77,11 @@ class UserSettingsDecorator def theme_preference settings['setting_theme'] end - + + def skin_preference + settings['setting_skin'] + end + def boolean_cast_setting(key) settings[key] == '1' end diff --git a/app/models/user.rb b/app/models/user.rb index b9b228c00..1d42d4f70 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -74,7 +74,7 @@ class User < ApplicationRecord has_many :session_activations, dependent: :destroy delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal, - :reduce_motion, :system_font_ui, :noindex, :theme, + :reduce_motion, :system_font_ui, :noindex, :theme, :skin, to: :settings, prefix: :setting, allow_nil: false def confirmed? diff --git a/app/views/layouts/_theme.html.haml b/app/views/layouts/_theme.html.haml index cdec4b370..941ccc914 100644 --- a/app/views/layouts/_theme.html.haml +++ b/app/views/layouts/_theme.html.haml @@ -3,8 +3,11 @@ = render partial: 'layouts/theme', object: theme[:common] - if theme[:pack] = javascript_pack_tag theme[:name] ? "themes/#{theme[:name]}/#{theme[:pack]}" : "core/#{theme[:pack]}", integrity: true, crossorigin: 'anonymous' - - if theme[:stylesheet] - = stylesheet_pack_tag theme[:name] ? "themes/#{theme[:name]}/#{theme[:pack]}" : "core/#{theme[:pack]}", integrity: true, media: 'all' + - if theme[:skin] + - if !theme[:name] || theme[:skin] == 'default' + = stylesheet_pack_tag theme[:name] ? "themes/#{theme[:name]}/#{theme[:pack]}" : "core/#{theme[:pack]}", integrity: true, media: 'all' + - else + = stylesheet_pack_tag "skins/#{theme[:name]}/#{theme[:skin]}/#{theme[:pack]}" - if theme[:preload] - theme[:preload].each do |link| %link{ href: asset_pack_path("#{link}.js"), crossorigin: 'anonymous', rel: 'preload', as: 'script' }/ diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index 69e26a7be..0d487d9f3 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -28,6 +28,7 @@ .fields-group - if Themes.instance.names.size > 1 = f.input :setting_theme, collection: Themes.instance.names, label_method: lambda { |theme| I18n.t("themes.#{theme}", default: theme) }, wrapper: :with_label, include_blank: false + = f.input :setting_skin, collection: Themes.instance.skins_for(current_theme), label_method: lambda { |skin| I18n.t("themes.#{current_theme}.skins.#{skin}", default: skin) }, wrapper: :with_label, include_blank: false = f.input :setting_unfollow_modal, as: :boolean, wrapper: :with_label = f.input :setting_boost_modal, as: :boolean, wrapper: :with_label diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index faf41f316..b9ef21fef 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -15,6 +15,7 @@ en: other: %{count} characters left setting_noindex: Affects your public profile and status pages setting_theme: Affects how Mastodon looks when you're logged in from any device. + setting_skin: Reskins the selected Mastodon theme imports: data: CSV file exported from another Mastodon instance sessions: @@ -47,6 +48,7 @@ en: setting_reduce_motion: Reduce motion in animations setting_system_font_ui: Use system's default font setting_theme: Site theme + setting_skin: Skin setting_unfollow_modal: Show confirmation dialog before unfollowing someone severity: Severity type: Import type diff --git a/config/settings.yml b/config/settings.yml index 6983484d0..5abf647e8 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -26,6 +26,7 @@ defaults: &defaults system_font_ui: false noindex: false theme: 'glitch' + skin: 'default' notification_emails: follow: false reblog: false diff --git a/config/webpack/configuration.js b/config/webpack/configuration.js index f8741c5d8..cb31c6ab8 100644 --- a/config/webpack/configuration.js +++ b/config/webpack/configuration.js @@ -1,15 +1,16 @@ // Common configuration for webpacker loaded from config/webpacker.yml -const { basename, dirname, join, resolve } = require('path'); +const { basename, dirname, extname, join, resolve } = require('path'); const { env } = require('process'); const { safeLoad } = require('js-yaml'); -const { readFileSync } = require('fs'); +const { lstatSync, readFileSync } = require('fs'); const glob = require('glob'); const configPath = resolve('config', 'webpacker.yml'); const loadersDir = join(__dirname, 'loaders'); const settings = safeLoad(readFileSync(configPath), 'utf8')[env.NODE_ENV]; const themeFiles = glob.sync('app/javascript/themes/*/theme.yml'); +const skinFiles = glob.sync('app/javascript/skins/*/*'); const themes = {}; const core = function () { @@ -25,14 +26,35 @@ for (let i = 0; i < themeFiles.length; i++) { const themeFile = themeFiles[i]; const data = safeLoad(readFileSync(themeFile), 'utf8'); data.name = basename(dirname(themeFile)); + data.skin = {}; if (!data.pack_directory) { data.pack_directory = dirname(themeFile); } - if (data.pack && typeof data.pack == 'object') { + if (data.pack && typeof data.pack === 'object') { themes[data.name] = data; } } +for (let i = 0; i < skinFiles.length; i++) { + const skinFile = skinFiles[i]; + let skin = basename(skinFile); + const name = basename(dirname(skinFile)); + if (!themes[name]) { + continue; + } + const data = themes[name].skin; + if (lstatSync(skinFile).isDirectory()) { + data[skin] = {}; + const skinPacks = glob.sync(skinFile, '*.{css,scss}'); + for (let j = 0; j < skinPacks.length; j++) { + const pack = skinPacks[i]; + data[skin][basename(pack, extname(pack))] = pack; + } + } else if ((skin = skin.match(/^(.*)\.s?css$/i))) { + data[skin[1]] = { common: skinFile }; + } +} + function removeOuterSlashes(string) { return string.replace(/^\/*/, '').replace(/\/*$/, ''); } diff --git a/config/webpack/shared.js b/config/webpack/shared.js index 5b90f27fb..a2550bc81 100644 --- a/config/webpack/shared.js +++ b/config/webpack/shared.js @@ -1,7 +1,7 @@ // Note: You must restart bin/webpack-dev-server for changes to take effect const webpack = require('webpack'); -const { basename, dirname, join, relative, resolve } = require('path'); +const { basename, join, resolve } = require('path'); const { sync } = require('glob'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); const ManifestPlugin = require('webpack-manifest-plugin'); @@ -24,6 +24,24 @@ function reducePacks (data, into = {}) { } return map; }, into); + if (data.name) { + Object.keys(data.skin).reduce((map, entry) => { + const skin = data.skin[entry]; + const skinName = entry; + if (!skin) { + return map; + } + Object.keys(skin).reduce((map, entry) => { + const packFile = skin[entry]; + if (!packFile) { + return map; + } + map[`skins/${data.name}/${skinName}/${entry}`] = resolve(packFile); + return map; + }, into); + return map; + }, into); + } return into; } -- cgit From bc4fa6b198557a7f3989eb0865e2c77ac7451d29 Mon Sep 17 00:00:00 2001 From: kibigo! Date: Sun, 3 Dec 2017 23:26:40 -0800 Subject: Rename themes -> flavours ? ? --- .gitmodules | 3 - app/controllers/application_controller.rb | 36 +- app/javascript/core/settings.js | 2 +- app/javascript/flavours/glitch/actions/accounts.js | 661 +++ app/javascript/flavours/glitch/actions/alerts.js | 24 + app/javascript/flavours/glitch/actions/blocks.js | 82 + app/javascript/flavours/glitch/actions/bundles.js | 25 + app/javascript/flavours/glitch/actions/cards.js | 52 + app/javascript/flavours/glitch/actions/columns.js | 40 + app/javascript/flavours/glitch/actions/compose.js | 398 ++ .../flavours/glitch/actions/domain_blocks.js | 117 + app/javascript/flavours/glitch/actions/emojis.js | 14 + .../flavours/glitch/actions/favourites.js | 83 + .../flavours/glitch/actions/height_cache.js | 17 + .../flavours/glitch/actions/interactions.js | 313 ++ .../flavours/glitch/actions/local_settings.js | 24 + app/javascript/flavours/glitch/actions/modal.js | 16 + app/javascript/flavours/glitch/actions/mutes.js | 103 + .../flavours/glitch/actions/notifications.js | 265 ++ .../flavours/glitch/actions/onboarding.js | 14 + .../flavours/glitch/actions/pin_statuses.js | 40 + .../flavours/glitch/actions/push_notifications.js | 52 + app/javascript/flavours/glitch/actions/reports.js | 80 + app/javascript/flavours/glitch/actions/search.js | 73 + app/javascript/flavours/glitch/actions/settings.js | 31 + app/javascript/flavours/glitch/actions/statuses.js | 217 + app/javascript/flavours/glitch/actions/store.js | 17 + .../flavours/glitch/actions/streaming.js | 54 + .../flavours/glitch/actions/timelines.js | 208 + .../flavours/glitch/components/account.js | 116 + .../flavours/glitch/components/attachment_list.js | 33 + .../glitch/components/autosuggest_emoji.js | 42 + .../glitch/components/autosuggest_textarea.js | 222 + .../flavours/glitch/components/avatar.js | 72 + .../flavours/glitch/components/avatar_overlay.js | 30 + .../flavours/glitch/components/button.js | 64 + .../flavours/glitch/components/collapsable.js | 22 + .../flavours/glitch/components/column.js | 54 + .../glitch/components/column_back_button.js | 29 + .../glitch/components/column_back_button_slim.js | 31 + .../flavours/glitch/components/column_header.js | 213 + .../flavours/glitch/components/display_name.js | 20 + .../flavours/glitch/components/dropdown_menu.js | 211 + .../glitch/components/extended_video_player.js | 54 + .../flavours/glitch/components/icon_button.js | 137 + .../components/intersection_observer_article.js | 130 + .../flavours/glitch/components/load_more.js | 26 + .../glitch/components/loading_indicator.js | 11 + .../flavours/glitch/components/media_gallery.js | 255 ++ .../glitch/components/missing_indicator.js | 12 + .../components/notification_purge_buttons.js | 58 + .../flavours/glitch/components/permalink.js | 34 + .../glitch/components/relative_timestamp.js | 147 + .../flavours/glitch/components/scrollable_list.js | 198 + .../flavours/glitch/components/setting_text.js | 34 + .../flavours/glitch/components/status.js | 442 ++ .../glitch/components/status_action_bar.js | 185 + .../flavours/glitch/components/status_content.js | 245 + .../flavours/glitch/components/status_header.js | 120 + .../flavours/glitch/components/status_list.js | 72 + .../flavours/glitch/components/status_prepend.js | 83 + .../glitch/components/status_visibility_icon.js | 48 + .../glitch/containers/account_container.js | 72 + .../flavours/glitch/containers/card_container.js | 18 + .../glitch/containers/compose_container.js | 38 + .../glitch/containers/dropdown_menu_container.js | 16 + .../intersection_observer_article_container.js | 17 + .../flavours/glitch/containers/mastodon.js | 70 + .../glitch/containers/media_gallery_container.js | 34 + .../notification_purge_buttons_container.js | 49 + .../flavours/glitch/containers/status_container.js | 150 + .../glitch/containers/timeline_container.js | 48 + .../flavours/glitch/containers/video_container.js | 26 + .../features/account/components/action_bar.js | 145 + .../glitch/features/account/components/header.js | 99 + .../account_gallery/components/media_item.js | 39 + .../glitch/features/account_gallery/index.js | 111 + .../features/account_timeline/components/header.js | 95 + .../containers/header_container.js | 104 + .../glitch/features/account_timeline/index.js | 77 + .../flavours/glitch/features/blocks/index.js | 70 + .../components/column_settings.js | 35 + .../containers/column_settings_container.js | 17 + .../glitch/features/community_timeline/index.js | 107 + .../compose/components/advanced_options.js | 62 + .../compose/components/advanced_options_toggle.js | 35 + .../features/compose/components/attach_options.js | 131 + .../compose/components/autosuggest_account.js | 24 + .../compose/components/character_counter.js | 25 + .../features/compose/components/compose_form.js | 286 ++ .../glitch/features/compose/components/dropdown.js | 77 + .../compose/components/emoji_picker_dropdown.js | 376 ++ .../features/compose/components/navigation_bar.js | 38 + .../compose/components/privacy_dropdown.js | 200 + .../features/compose/components/reply_indicator.js | 63 + .../glitch/features/compose/components/search.js | 129 + .../features/compose/components/search_results.js | 65 + .../compose/components/text_icon_button.js | 29 + .../glitch/features/compose/components/upload.js | 96 + .../features/compose/components/upload_button.js | 77 + .../features/compose/components/upload_form.js | 29 + .../features/compose/components/upload_progress.js | 42 + .../glitch/features/compose/components/warning.js | 26 + .../containers/advanced_options_container.js | 20 + .../containers/autosuggest_account_container.js | 15 + .../compose/containers/compose_form_container.js | 71 + .../containers/emoji_picker_dropdown_container.js | 82 + .../compose/containers/navigation_container.js | 11 + .../containers/privacy_dropdown_container.js | 24 + .../containers/reply_indicator_container.js | 24 + .../compose/containers/search_container.js | 35 + .../compose/containers/search_results_container.js | 8 + .../containers/sensitive_button_container.js | 71 + .../compose/containers/spoiler_button_container.js | 25 + .../compose/containers/upload_button_container.js | 18 + .../compose/containers/upload_container.js | 21 + .../compose/containers/upload_form_container.js | 8 + .../containers/upload_progress_container.js | 9 + .../compose/containers/warning_container.js | 24 + .../flavours/glitch/features/compose/index.js | 126 + .../containers/column_settings_container.js | 17 + .../glitch/features/direct_timeline/index.js | 107 + .../glitch/features/favourited_statuses/index.js | 94 + .../flavours/glitch/features/favourites/index.js | 60 + .../components/account_authorize.js | 49 + .../containers/account_authorize_container.js | 26 + .../glitch/features/follow_requests/index.js | 71 + .../flavours/glitch/features/followers/index.js | 93 + .../flavours/glitch/features/following/index.js | 93 + .../glitch/features/generic_not_found/index.js | 11 + .../glitch/features/getting_started/index.js | 145 + .../glitch/features/hashtag_timeline/index.js | 118 + .../home_timeline/components/column_settings.js | 46 + .../containers/column_settings_container.js | 21 + .../glitch/features/home_timeline/index.js | 90 + .../glitch/features/local_settings/index.js | 68 + .../features/local_settings/navigation/index.js | 74 + .../local_settings/navigation/item/index.js | 69 + .../local_settings/navigation/item/style.scss | 27 + .../features/local_settings/navigation/style.scss | 10 + .../glitch/features/local_settings/page/index.js | 212 + .../features/local_settings/page/item/index.js | 90 + .../features/local_settings/page/item/style.scss | 7 + .../glitch/features/local_settings/page/style.scss | 9 + .../glitch/features/local_settings/style.scss | 34 + .../flavours/glitch/features/mutes/index.js | 70 + .../components/clear_column_button.js | 17 + .../notifications/components/column_settings.js | 86 + .../features/notifications/components/follow.js | 97 + .../notifications/components/notification.js | 88 + .../features/notifications/components/overlay.js | 57 + .../notifications/components/setting_toggle.js | 34 + .../containers/column_settings_container.js | 44 + .../containers/notification_container.js | 27 + .../notifications/containers/overlay_container.js | 18 + .../glitch/features/notifications/index.js | 193 + .../glitch/features/pinned_statuses/index.js | 59 + .../containers/column_settings_container.js | 17 + .../glitch/features/public_timeline/index.js | 107 + .../flavours/glitch/features/reblogs/index.js | 60 + .../features/report/components/status_check_box.js | 37 + .../containers/status_check_box_container.js | 19 + .../glitch/features/standalone/compose/index.js | 20 + .../features/standalone/hashtag_timeline/index.js | 70 + .../features/standalone/public_timeline/index.js | 76 + .../features/status/components/action_bar.js | 129 + .../glitch/features/status/components/card.js | 125 + .../features/status/components/detailed_status.js | 128 + .../features/status/containers/card_container.js | 8 + .../flavours/glitch/features/status/index.js | 343 ++ .../glitch/features/ui/components/actions_modal.js | 74 + .../glitch/features/ui/components/boost_modal.js | 84 + .../glitch/features/ui/components/bundle.js | 102 + .../features/ui/components/bundle_column_error.js | 44 + .../features/ui/components/bundle_modal_error.js | 53 + .../glitch/features/ui/components/column.js | 74 + .../glitch/features/ui/components/column_header.js | 35 + .../glitch/features/ui/components/column_link.js | 39 + .../features/ui/components/column_loading.js | 30 + .../features/ui/components/column_subheading.js | 16 + .../glitch/features/ui/components/columns_area.js | 174 + .../features/ui/components/confirmation_modal.js | 53 + .../glitch/features/ui/components/doodle_modal.js | 614 +++ .../features/ui/components/drawer_loading.js | 11 + .../glitch/features/ui/components/embed_modal.js | 84 + .../glitch/features/ui/components/image_loader.js | 152 + .../glitch/features/ui/components/media_modal.js | 126 + .../glitch/features/ui/components/modal_loading.js | 20 + .../glitch/features/ui/components/modal_root.js | 131 + .../glitch/features/ui/components/mute_modal.js | 105 + .../features/ui/components/onboarding_modal.js | 323 ++ .../glitch/features/ui/components/report_modal.js | 105 + .../glitch/features/ui/components/tabs_bar.js | 84 + .../glitch/features/ui/components/upload_area.js | 52 + .../glitch/features/ui/components/video_modal.js | 33 + .../features/ui/containers/bundle_container.js | 19 + .../ui/containers/columns_area_container.js | 8 + .../ui/containers/loading_bar_container.js | 8 + .../features/ui/containers/modal_container.js | 16 + .../ui/containers/notifications_container.js | 18 + .../ui/containers/status_list_container.js | 73 + .../flavours/glitch/features/ui/index.js | 442 ++ .../flavours/glitch/features/video/index.js | 288 ++ .../flavours/glitch/middleware/errors.js | 31 + .../flavours/glitch/middleware/loading_bar.js | 25 + .../flavours/glitch/middleware/sounds.js | 46 + app/javascript/flavours/glitch/packs/about.js | 22 + app/javascript/flavours/glitch/packs/common.js | 1 + app/javascript/flavours/glitch/packs/home.js | 7 + app/javascript/flavours/glitch/packs/public.js | 75 + app/javascript/flavours/glitch/packs/share.js | 22 + .../flavours/glitch/reducers/accounts.js | 135 + .../flavours/glitch/reducers/accounts_counters.js | 138 + app/javascript/flavours/glitch/reducers/alerts.js | 25 + app/javascript/flavours/glitch/reducers/cards.js | 14 + app/javascript/flavours/glitch/reducers/compose.js | 307 ++ .../flavours/glitch/reducers/contexts.js | 61 + .../flavours/glitch/reducers/custom_emojis.js | 16 + .../flavours/glitch/reducers/height_cache.js | 23 + app/javascript/flavours/glitch/reducers/index.js | 54 + .../flavours/glitch/reducers/local_settings.js | 45 + .../flavours/glitch/reducers/media_attachments.js | 15 + app/javascript/flavours/glitch/reducers/meta.js | 16 + app/javascript/flavours/glitch/reducers/modal.js | 17 + app/javascript/flavours/glitch/reducers/mutes.js | 29 + .../flavours/glitch/reducers/notifications.js | 191 + .../flavours/glitch/reducers/push_notifications.js | 51 + .../flavours/glitch/reducers/relationships.js | 46 + app/javascript/flavours/glitch/reducers/reports.js | 60 + app/javascript/flavours/glitch/reducers/search.js | 42 + .../flavours/glitch/reducers/settings.js | 119 + .../flavours/glitch/reducers/status_lists.js | 75 + .../flavours/glitch/reducers/statuses.js | 148 + .../flavours/glitch/reducers/timelines.js | 149 + .../flavours/glitch/reducers/user_lists.js | 80 + app/javascript/flavours/glitch/selectors/index.js | 87 + .../flavours/glitch/service_worker/entry.js | 10 + .../service_worker/web_push_notifications.js | 159 + .../flavours/glitch/store/configureStore.js | 15 + app/javascript/flavours/glitch/styles/_mixins.scss | 51 + app/javascript/flavours/glitch/styles/about.scss | 822 ++++ .../flavours/glitch/styles/accounts.scss | 589 +++ app/javascript/flavours/glitch/styles/admin.scss | 349 ++ app/javascript/flavours/glitch/styles/basics.scss | 122 + app/javascript/flavours/glitch/styles/boost.scss | 28 + .../flavours/glitch/styles/compact_header.scss | 34 + .../flavours/glitch/styles/components.scss | 4832 ++++++++++++++++++++ .../flavours/glitch/styles/containers.scss | 116 + app/javascript/flavours/glitch/styles/doodle.scss | 86 + .../flavours/glitch/styles/emoji_picker.scss | 199 + app/javascript/flavours/glitch/styles/footer.scss | 30 + app/javascript/flavours/glitch/styles/forms.scss | 540 +++ app/javascript/flavours/glitch/styles/index.scss | 22 + .../flavours/glitch/styles/landing_strip.scss | 36 + app/javascript/flavours/glitch/styles/lists.scss | 19 + .../flavours/glitch/styles/reset copy.scss | 91 + app/javascript/flavours/glitch/styles/reset.scss | 91 + app/javascript/flavours/glitch/styles/rtl.scss | 254 + .../flavours/glitch/styles/stream_entries.scss | 335 ++ app/javascript/flavours/glitch/styles/tables.scss | 76 + .../flavours/glitch/styles/variables.scss | 35 + app/javascript/flavours/glitch/theme.yml | 34 + app/javascript/flavours/glitch/util/api.js | 26 + .../flavours/glitch/util/async-components.js | 115 + .../flavours/glitch/util/base_polyfills.js | 18 + .../flavours/glitch/util/bio_metadata.js | 331 ++ app/javascript/flavours/glitch/util/counter.js | 9 + .../flavours/glitch/util/emoji/emoji_compressed.js | 93 + .../flavours/glitch/util/emoji/emoji_map.json | 1 + .../glitch/util/emoji/emoji_mart_data_light.js | 41 + .../glitch/util/emoji/emoji_mart_search_light.js | 157 + .../flavours/glitch/util/emoji/emoji_picker.js | 7 + .../util/emoji/emoji_unicode_mapping_light.js | 35 + .../flavours/glitch/util/emoji/emoji_utils.js | 258 ++ app/javascript/flavours/glitch/util/emoji/index.js | 95 + .../glitch/util/emoji/unicode_to_filename.js | 26 + .../glitch/util/emoji/unicode_to_unified_name.js | 17 + .../flavours/glitch/util/extra_polyfills.js | 5 + app/javascript/flavours/glitch/util/fullscreen.js | 46 + .../flavours/glitch/util/get_rect_from_entry.js | 21 + .../flavours/glitch/util/initial_state.js | 21 + .../glitch/util/intersection_observer_wrapper.js | 57 + app/javascript/flavours/glitch/util/is_mobile.js | 34 + app/javascript/flavours/glitch/util/link_header.js | 33 + .../flavours/glitch/util/load_polyfills.js | 39 + app/javascript/flavours/glitch/util/main.js | 39 + .../flavours/glitch/util/optional_motion.js | 5 + app/javascript/flavours/glitch/util/performance.js | 31 + .../flavours/glitch/util/react_router_helpers.js | 64 + app/javascript/flavours/glitch/util/ready.js | 7 + .../flavours/glitch/util/reduced_motion.js | 44 + app/javascript/flavours/glitch/util/rtl.js | 31 + .../flavours/glitch/util/schedule_idle_task.js | 29 + app/javascript/flavours/glitch/util/scroll.js | 30 + app/javascript/flavours/glitch/util/stream.js | 73 + app/javascript/flavours/glitch/util/url_regex.js | 196 + app/javascript/flavours/glitch/util/uuid.js | 3 + .../flavours/glitch/util/web_push_subscription.js | 105 + app/javascript/flavours/vanilla/theme.yml | 33 + app/javascript/themes/glitch/actions/accounts.js | 661 --- app/javascript/themes/glitch/actions/alerts.js | 24 - app/javascript/themes/glitch/actions/blocks.js | 82 - app/javascript/themes/glitch/actions/bundles.js | 25 - app/javascript/themes/glitch/actions/cards.js | 52 - app/javascript/themes/glitch/actions/columns.js | 40 - app/javascript/themes/glitch/actions/compose.js | 398 -- .../themes/glitch/actions/domain_blocks.js | 117 - app/javascript/themes/glitch/actions/emojis.js | 14 - app/javascript/themes/glitch/actions/favourites.js | 83 - .../themes/glitch/actions/height_cache.js | 17 - .../themes/glitch/actions/interactions.js | 313 -- .../themes/glitch/actions/local_settings.js | 24 - app/javascript/themes/glitch/actions/modal.js | 16 - app/javascript/themes/glitch/actions/mutes.js | 103 - .../themes/glitch/actions/notifications.js | 265 -- app/javascript/themes/glitch/actions/onboarding.js | 14 - .../themes/glitch/actions/pin_statuses.js | 40 - .../themes/glitch/actions/push_notifications.js | 52 - app/javascript/themes/glitch/actions/reports.js | 80 - app/javascript/themes/glitch/actions/search.js | 73 - app/javascript/themes/glitch/actions/settings.js | 31 - app/javascript/themes/glitch/actions/statuses.js | 217 - app/javascript/themes/glitch/actions/store.js | 17 - app/javascript/themes/glitch/actions/streaming.js | 54 - app/javascript/themes/glitch/actions/timelines.js | 208 - app/javascript/themes/glitch/components/account.js | 116 - .../themes/glitch/components/attachment_list.js | 33 - .../themes/glitch/components/autosuggest_emoji.js | 42 - .../glitch/components/autosuggest_textarea.js | 222 - app/javascript/themes/glitch/components/avatar.js | 72 - .../themes/glitch/components/avatar_overlay.js | 30 - app/javascript/themes/glitch/components/button.js | 64 - .../themes/glitch/components/collapsable.js | 22 - app/javascript/themes/glitch/components/column.js | 54 - .../themes/glitch/components/column_back_button.js | 29 - .../glitch/components/column_back_button_slim.js | 31 - .../themes/glitch/components/column_header.js | 214 - .../themes/glitch/components/display_name.js | 20 - .../themes/glitch/components/dropdown_menu.js | 211 - .../glitch/components/extended_video_player.js | 54 - .../themes/glitch/components/icon_button.js | 137 - .../components/intersection_observer_article.js | 130 - .../themes/glitch/components/load_more.js | 26 - .../themes/glitch/components/loading_indicator.js | 11 - .../themes/glitch/components/media_gallery.js | 255 -- .../themes/glitch/components/missing_indicator.js | 12 - .../components/notification_purge_buttons.js | 58 - .../themes/glitch/components/permalink.js | 34 - .../themes/glitch/components/relative_timestamp.js | 147 - .../themes/glitch/components/scrollable_list.js | 198 - .../themes/glitch/components/setting_text.js | 34 - app/javascript/themes/glitch/components/status.js | 442 -- .../themes/glitch/components/status_action_bar.js | 188 - .../themes/glitch/components/status_content.js | 245 - .../themes/glitch/components/status_header.js | 120 - .../themes/glitch/components/status_list.js | 72 - .../themes/glitch/components/status_prepend.js | 83 - .../glitch/components/status_visibility_icon.js | 48 - .../themes/glitch/containers/account_container.js | 72 - .../themes/glitch/containers/card_container.js | 18 - .../themes/glitch/containers/compose_container.js | 38 - .../glitch/containers/dropdown_menu_container.js | 16 - .../intersection_observer_article_container.js | 17 - .../themes/glitch/containers/mastodon.js | 70 - .../glitch/containers/media_gallery_container.js | 34 - .../notification_purge_buttons_container.js | 49 - .../themes/glitch/containers/status_container.js | 150 - .../themes/glitch/containers/timeline_container.js | 48 - .../themes/glitch/containers/video_container.js | 26 - .../features/account/components/action_bar.js | 145 - .../glitch/features/account/components/header.js | 99 - .../account_gallery/components/media_item.js | 39 - .../glitch/features/account_gallery/index.js | 111 - .../features/account_timeline/components/header.js | 95 - .../containers/header_container.js | 104 - .../glitch/features/account_timeline/index.js | 77 - .../themes/glitch/features/blocks/index.js | 70 - .../components/column_settings.js | 35 - .../containers/column_settings_container.js | 17 - .../glitch/features/community_timeline/index.js | 107 - .../compose/components/advanced_options.js | 62 - .../compose/components/advanced_options_toggle.js | 35 - .../features/compose/components/attach_options.js | 131 - .../compose/components/autosuggest_account.js | 24 - .../compose/components/character_counter.js | 25 - .../features/compose/components/compose_form.js | 286 -- .../glitch/features/compose/components/dropdown.js | 77 - .../compose/components/emoji_picker_dropdown.js | 376 -- .../features/compose/components/navigation_bar.js | 38 - .../compose/components/privacy_dropdown.js | 200 - .../features/compose/components/reply_indicator.js | 63 - .../glitch/features/compose/components/search.js | 129 - .../features/compose/components/search_results.js | 65 - .../compose/components/text_icon_button.js | 29 - .../glitch/features/compose/components/upload.js | 96 - .../features/compose/components/upload_button.js | 77 - .../features/compose/components/upload_form.js | 29 - .../features/compose/components/upload_progress.js | 42 - .../glitch/features/compose/components/warning.js | 26 - .../containers/advanced_options_container.js | 20 - .../containers/autosuggest_account_container.js | 15 - .../compose/containers/compose_form_container.js | 71 - .../containers/emoji_picker_dropdown_container.js | 82 - .../compose/containers/navigation_container.js | 11 - .../containers/privacy_dropdown_container.js | 24 - .../containers/reply_indicator_container.js | 24 - .../compose/containers/search_container.js | 35 - .../compose/containers/search_results_container.js | 8 - .../containers/sensitive_button_container.js | 71 - .../compose/containers/spoiler_button_container.js | 25 - .../compose/containers/upload_button_container.js | 18 - .../compose/containers/upload_container.js | 21 - .../compose/containers/upload_form_container.js | 8 - .../containers/upload_progress_container.js | 9 - .../compose/containers/warning_container.js | 24 - .../themes/glitch/features/compose/index.js | 126 - .../containers/column_settings_container.js | 17 - .../glitch/features/direct_timeline/index.js | 107 - .../glitch/features/favourited_statuses/index.js | 94 - .../themes/glitch/features/favourites/index.js | 60 - .../components/account_authorize.js | 49 - .../containers/account_authorize_container.js | 26 - .../glitch/features/follow_requests/index.js | 71 - .../themes/glitch/features/followers/index.js | 93 - .../themes/glitch/features/following/index.js | 93 - .../glitch/features/generic_not_found/index.js | 11 - .../glitch/features/getting_started/index.js | 145 - .../glitch/features/hashtag_timeline/index.js | 118 - .../home_timeline/components/column_settings.js | 46 - .../containers/column_settings_container.js | 21 - .../themes/glitch/features/home_timeline/index.js | 90 - .../themes/glitch/features/local_settings/index.js | 68 - .../features/local_settings/navigation/index.js | 74 - .../local_settings/navigation/item/index.js | 69 - .../local_settings/navigation/item/style.scss | 27 - .../features/local_settings/navigation/style.scss | 10 - .../glitch/features/local_settings/page/index.js | 212 - .../features/local_settings/page/item/index.js | 90 - .../features/local_settings/page/item/style.scss | 7 - .../glitch/features/local_settings/page/style.scss | 9 - .../glitch/features/local_settings/style.scss | 34 - .../themes/glitch/features/mutes/index.js | 70 - .../components/clear_column_button.js | 17 - .../notifications/components/column_settings.js | 86 - .../features/notifications/components/follow.js | 97 - .../notifications/components/notification.js | 88 - .../features/notifications/components/overlay.js | 57 - .../notifications/components/setting_toggle.js | 34 - .../containers/column_settings_container.js | 44 - .../containers/notification_container.js | 27 - .../notifications/containers/overlay_container.js | 18 - .../themes/glitch/features/notifications/index.js | 193 - .../glitch/features/pinned_statuses/index.js | 59 - .../containers/column_settings_container.js | 17 - .../glitch/features/public_timeline/index.js | 107 - .../themes/glitch/features/reblogs/index.js | 60 - .../features/report/components/status_check_box.js | 37 - .../containers/status_check_box_container.js | 19 - .../glitch/features/standalone/compose/index.js | 20 - .../features/standalone/hashtag_timeline/index.js | 70 - .../features/standalone/public_timeline/index.js | 76 - .../features/status/components/action_bar.js | 129 - .../glitch/features/status/components/card.js | 125 - .../features/status/components/detailed_status.js | 128 - .../features/status/containers/card_container.js | 8 - .../themes/glitch/features/status/index.js | 343 -- .../glitch/features/ui/components/actions_modal.js | 74 - .../glitch/features/ui/components/boost_modal.js | 84 - .../themes/glitch/features/ui/components/bundle.js | 102 - .../features/ui/components/bundle_column_error.js | 44 - .../features/ui/components/bundle_modal_error.js | 53 - .../themes/glitch/features/ui/components/column.js | 74 - .../glitch/features/ui/components/column_header.js | 35 - .../glitch/features/ui/components/column_link.js | 39 - .../features/ui/components/column_loading.js | 30 - .../features/ui/components/column_subheading.js | 16 - .../glitch/features/ui/components/columns_area.js | 174 - .../features/ui/components/confirmation_modal.js | 53 - .../glitch/features/ui/components/doodle_modal.js | 614 --- .../features/ui/components/drawer_loading.js | 11 - .../glitch/features/ui/components/embed_modal.js | 84 - .../glitch/features/ui/components/image_loader.js | 152 - .../glitch/features/ui/components/media_modal.js | 126 - .../glitch/features/ui/components/modal_loading.js | 20 - .../glitch/features/ui/components/modal_root.js | 131 - .../glitch/features/ui/components/mute_modal.js | 105 - .../features/ui/components/onboarding_modal.js | 323 -- .../glitch/features/ui/components/report_modal.js | 105 - .../glitch/features/ui/components/tabs_bar.js | 84 - .../glitch/features/ui/components/upload_area.js | 52 - .../glitch/features/ui/components/video_modal.js | 33 - .../features/ui/containers/bundle_container.js | 19 - .../ui/containers/columns_area_container.js | 8 - .../ui/containers/loading_bar_container.js | 8 - .../features/ui/containers/modal_container.js | 16 - .../ui/containers/notifications_container.js | 18 - .../ui/containers/status_list_container.js | 73 - app/javascript/themes/glitch/features/ui/index.js | 442 -- .../themes/glitch/features/video/index.js | 288 -- app/javascript/themes/glitch/middleware/errors.js | 31 - .../themes/glitch/middleware/loading_bar.js | 25 - app/javascript/themes/glitch/middleware/sounds.js | 46 - app/javascript/themes/glitch/packs/about.js | 22 - app/javascript/themes/glitch/packs/common.js | 1 - app/javascript/themes/glitch/packs/home.js | 7 - app/javascript/themes/glitch/packs/public.js | 75 - app/javascript/themes/glitch/packs/share.js | 22 - app/javascript/themes/glitch/reducers/accounts.js | 135 - .../themes/glitch/reducers/accounts_counters.js | 138 - app/javascript/themes/glitch/reducers/alerts.js | 25 - app/javascript/themes/glitch/reducers/cards.js | 14 - app/javascript/themes/glitch/reducers/compose.js | 307 -- app/javascript/themes/glitch/reducers/contexts.js | 61 - .../themes/glitch/reducers/custom_emojis.js | 16 - .../themes/glitch/reducers/height_cache.js | 23 - app/javascript/themes/glitch/reducers/index.js | 54 - .../themes/glitch/reducers/local_settings.js | 45 - .../themes/glitch/reducers/media_attachments.js | 15 - app/javascript/themes/glitch/reducers/meta.js | 16 - app/javascript/themes/glitch/reducers/modal.js | 17 - app/javascript/themes/glitch/reducers/mutes.js | 29 - .../themes/glitch/reducers/notifications.js | 191 - .../themes/glitch/reducers/push_notifications.js | 51 - .../themes/glitch/reducers/relationships.js | 46 - app/javascript/themes/glitch/reducers/reports.js | 60 - app/javascript/themes/glitch/reducers/search.js | 42 - app/javascript/themes/glitch/reducers/settings.js | 119 - .../themes/glitch/reducers/status_lists.js | 75 - app/javascript/themes/glitch/reducers/statuses.js | 148 - app/javascript/themes/glitch/reducers/timelines.js | 149 - .../themes/glitch/reducers/user_lists.js | 80 - app/javascript/themes/glitch/selectors/index.js | 87 - .../themes/glitch/service_worker/entry.js | 10 - .../service_worker/web_push_notifications.js | 159 - .../themes/glitch/store/configureStore.js | 15 - app/javascript/themes/glitch/styles/_mixins.scss | 51 - app/javascript/themes/glitch/styles/about.scss | 822 ---- app/javascript/themes/glitch/styles/accounts.scss | 589 --- app/javascript/themes/glitch/styles/admin.scss | 349 -- app/javascript/themes/glitch/styles/basics.scss | 122 - app/javascript/themes/glitch/styles/boost.scss | 28 - .../themes/glitch/styles/compact_header.scss | 34 - .../themes/glitch/styles/components.scss | 4832 -------------------- .../themes/glitch/styles/containers.scss | 116 - app/javascript/themes/glitch/styles/doodle.scss | 86 - .../themes/glitch/styles/emoji_picker.scss | 199 - app/javascript/themes/glitch/styles/footer.scss | 30 - app/javascript/themes/glitch/styles/forms.scss | 540 --- app/javascript/themes/glitch/styles/index.scss | 22 - .../themes/glitch/styles/landing_strip.scss | 36 - app/javascript/themes/glitch/styles/lists.scss | 19 - .../themes/glitch/styles/reset copy.scss | 91 - app/javascript/themes/glitch/styles/reset.scss | 91 - app/javascript/themes/glitch/styles/rtl.scss | 254 - .../themes/glitch/styles/stream_entries.scss | 335 -- app/javascript/themes/glitch/styles/tables.scss | 76 - app/javascript/themes/glitch/styles/variables.scss | 35 - app/javascript/themes/glitch/theme.yml | 34 - app/javascript/themes/glitch/util/api.js | 26 - .../themes/glitch/util/async-components.js | 115 - .../themes/glitch/util/base_polyfills.js | 18 - app/javascript/themes/glitch/util/bio_metadata.js | 331 -- app/javascript/themes/glitch/util/counter.js | 9 - .../themes/glitch/util/emoji/emoji_compressed.js | 93 - .../themes/glitch/util/emoji/emoji_map.json | 1 - .../glitch/util/emoji/emoji_mart_data_light.js | 41 - .../glitch/util/emoji/emoji_mart_search_light.js | 157 - .../themes/glitch/util/emoji/emoji_picker.js | 7 - .../util/emoji/emoji_unicode_mapping_light.js | 35 - .../themes/glitch/util/emoji/emoji_utils.js | 258 -- app/javascript/themes/glitch/util/emoji/index.js | 95 - .../glitch/util/emoji/unicode_to_filename.js | 26 - .../glitch/util/emoji/unicode_to_unified_name.js | 17 - .../themes/glitch/util/extra_polyfills.js | 5 - app/javascript/themes/glitch/util/fullscreen.js | 46 - .../themes/glitch/util/get_rect_from_entry.js | 21 - app/javascript/themes/glitch/util/initial_state.js | 21 - .../glitch/util/intersection_observer_wrapper.js | 57 - app/javascript/themes/glitch/util/is_mobile.js | 34 - app/javascript/themes/glitch/util/link_header.js | 33 - .../themes/glitch/util/load_polyfills.js | 39 - app/javascript/themes/glitch/util/main.js | 39 - .../themes/glitch/util/optional_motion.js | 5 - app/javascript/themes/glitch/util/performance.js | 31 - .../themes/glitch/util/react_router_helpers.js | 64 - app/javascript/themes/glitch/util/ready.js | 7 - .../themes/glitch/util/reduced_motion.js | 44 - app/javascript/themes/glitch/util/rtl.js | 31 - .../themes/glitch/util/schedule_idle_task.js | 29 - app/javascript/themes/glitch/util/scroll.js | 30 - app/javascript/themes/glitch/util/stream.js | 73 - app/javascript/themes/glitch/util/url_regex.js | 196 - app/javascript/themes/glitch/util/uuid.js | 3 - .../themes/glitch/util/web_push_subscription.js | 105 - app/javascript/themes/mastodon-go | 1 - app/javascript/themes/vanilla/theme.yml | 33 - app/lib/themes.rb | 6 +- app/models/user.rb | 2 +- app/views/layouts/_theme.html.haml | 6 +- app/views/settings/preferences/show.html.haml | 2 +- config/locales/simple_form.en.yml | 6 +- config/settings.yml | 2 +- config/webpack/configuration.js | 22 +- config/webpack/shared.js | 6 +- 604 files changed, 30732 insertions(+), 30748 deletions(-) create mode 100644 app/javascript/flavours/glitch/actions/accounts.js create mode 100644 app/javascript/flavours/glitch/actions/alerts.js create mode 100644 app/javascript/flavours/glitch/actions/blocks.js create mode 100644 app/javascript/flavours/glitch/actions/bundles.js create mode 100644 app/javascript/flavours/glitch/actions/cards.js create mode 100644 app/javascript/flavours/glitch/actions/columns.js create mode 100644 app/javascript/flavours/glitch/actions/compose.js create mode 100644 app/javascript/flavours/glitch/actions/domain_blocks.js create mode 100644 app/javascript/flavours/glitch/actions/emojis.js create mode 100644 app/javascript/flavours/glitch/actions/favourites.js create mode 100644 app/javascript/flavours/glitch/actions/height_cache.js create mode 100644 app/javascript/flavours/glitch/actions/interactions.js create mode 100644 app/javascript/flavours/glitch/actions/local_settings.js create mode 100644 app/javascript/flavours/glitch/actions/modal.js create mode 100644 app/javascript/flavours/glitch/actions/mutes.js create mode 100644 app/javascript/flavours/glitch/actions/notifications.js create mode 100644 app/javascript/flavours/glitch/actions/onboarding.js create mode 100644 app/javascript/flavours/glitch/actions/pin_statuses.js create mode 100644 app/javascript/flavours/glitch/actions/push_notifications.js create mode 100644 app/javascript/flavours/glitch/actions/reports.js create mode 100644 app/javascript/flavours/glitch/actions/search.js create mode 100644 app/javascript/flavours/glitch/actions/settings.js create mode 100644 app/javascript/flavours/glitch/actions/statuses.js create mode 100644 app/javascript/flavours/glitch/actions/store.js create mode 100644 app/javascript/flavours/glitch/actions/streaming.js create mode 100644 app/javascript/flavours/glitch/actions/timelines.js create mode 100644 app/javascript/flavours/glitch/components/account.js create mode 100644 app/javascript/flavours/glitch/components/attachment_list.js create mode 100644 app/javascript/flavours/glitch/components/autosuggest_emoji.js create mode 100644 app/javascript/flavours/glitch/components/autosuggest_textarea.js create mode 100644 app/javascript/flavours/glitch/components/avatar.js create mode 100644 app/javascript/flavours/glitch/components/avatar_overlay.js create mode 100644 app/javascript/flavours/glitch/components/button.js create mode 100644 app/javascript/flavours/glitch/components/collapsable.js create mode 100644 app/javascript/flavours/glitch/components/column.js create mode 100644 app/javascript/flavours/glitch/components/column_back_button.js create mode 100644 app/javascript/flavours/glitch/components/column_back_button_slim.js create mode 100644 app/javascript/flavours/glitch/components/column_header.js create mode 100644 app/javascript/flavours/glitch/components/display_name.js create mode 100644 app/javascript/flavours/glitch/components/dropdown_menu.js create mode 100644 app/javascript/flavours/glitch/components/extended_video_player.js create mode 100644 app/javascript/flavours/glitch/components/icon_button.js create mode 100644 app/javascript/flavours/glitch/components/intersection_observer_article.js create mode 100644 app/javascript/flavours/glitch/components/load_more.js create mode 100644 app/javascript/flavours/glitch/components/loading_indicator.js create mode 100644 app/javascript/flavours/glitch/components/media_gallery.js create mode 100644 app/javascript/flavours/glitch/components/missing_indicator.js create mode 100644 app/javascript/flavours/glitch/components/notification_purge_buttons.js create mode 100644 app/javascript/flavours/glitch/components/permalink.js create mode 100644 app/javascript/flavours/glitch/components/relative_timestamp.js create mode 100644 app/javascript/flavours/glitch/components/scrollable_list.js create mode 100644 app/javascript/flavours/glitch/components/setting_text.js create mode 100644 app/javascript/flavours/glitch/components/status.js create mode 100644 app/javascript/flavours/glitch/components/status_action_bar.js create mode 100644 app/javascript/flavours/glitch/components/status_content.js create mode 100644 app/javascript/flavours/glitch/components/status_header.js create mode 100644 app/javascript/flavours/glitch/components/status_list.js create mode 100644 app/javascript/flavours/glitch/components/status_prepend.js create mode 100644 app/javascript/flavours/glitch/components/status_visibility_icon.js create mode 100644 app/javascript/flavours/glitch/containers/account_container.js create mode 100644 app/javascript/flavours/glitch/containers/card_container.js create mode 100644 app/javascript/flavours/glitch/containers/compose_container.js create mode 100644 app/javascript/flavours/glitch/containers/dropdown_menu_container.js create mode 100644 app/javascript/flavours/glitch/containers/intersection_observer_article_container.js create mode 100644 app/javascript/flavours/glitch/containers/mastodon.js create mode 100644 app/javascript/flavours/glitch/containers/media_gallery_container.js create mode 100644 app/javascript/flavours/glitch/containers/notification_purge_buttons_container.js create mode 100644 app/javascript/flavours/glitch/containers/status_container.js create mode 100644 app/javascript/flavours/glitch/containers/timeline_container.js create mode 100644 app/javascript/flavours/glitch/containers/video_container.js create mode 100644 app/javascript/flavours/glitch/features/account/components/action_bar.js create mode 100644 app/javascript/flavours/glitch/features/account/components/header.js create mode 100644 app/javascript/flavours/glitch/features/account_gallery/components/media_item.js create mode 100644 app/javascript/flavours/glitch/features/account_gallery/index.js create mode 100644 app/javascript/flavours/glitch/features/account_timeline/components/header.js create mode 100644 app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js create mode 100644 app/javascript/flavours/glitch/features/account_timeline/index.js create mode 100644 app/javascript/flavours/glitch/features/blocks/index.js create mode 100644 app/javascript/flavours/glitch/features/community_timeline/components/column_settings.js create mode 100644 app/javascript/flavours/glitch/features/community_timeline/containers/column_settings_container.js create mode 100644 app/javascript/flavours/glitch/features/community_timeline/index.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/advanced_options.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/advanced_options_toggle.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/attach_options.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/autosuggest_account.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/character_counter.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/compose_form.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/dropdown.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/navigation_bar.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/reply_indicator.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/search.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/search_results.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/text_icon_button.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/upload.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/upload_button.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/upload_form.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/upload_progress.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/warning.js create mode 100644 app/javascript/flavours/glitch/features/compose/containers/advanced_options_container.js create mode 100644 app/javascript/flavours/glitch/features/compose/containers/autosuggest_account_container.js create mode 100644 app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js create mode 100644 app/javascript/flavours/glitch/features/compose/containers/emoji_picker_dropdown_container.js create mode 100644 app/javascript/flavours/glitch/features/compose/containers/navigation_container.js create mode 100644 app/javascript/flavours/glitch/features/compose/containers/privacy_dropdown_container.js create mode 100644 app/javascript/flavours/glitch/features/compose/containers/reply_indicator_container.js create mode 100644 app/javascript/flavours/glitch/features/compose/containers/search_container.js create mode 100644 app/javascript/flavours/glitch/features/compose/containers/search_results_container.js create mode 100644 app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.js create mode 100644 app/javascript/flavours/glitch/features/compose/containers/spoiler_button_container.js create mode 100644 app/javascript/flavours/glitch/features/compose/containers/upload_button_container.js create mode 100644 app/javascript/flavours/glitch/features/compose/containers/upload_container.js create mode 100644 app/javascript/flavours/glitch/features/compose/containers/upload_form_container.js create mode 100644 app/javascript/flavours/glitch/features/compose/containers/upload_progress_container.js create mode 100644 app/javascript/flavours/glitch/features/compose/containers/warning_container.js create mode 100644 app/javascript/flavours/glitch/features/compose/index.js create mode 100644 app/javascript/flavours/glitch/features/direct_timeline/containers/column_settings_container.js create mode 100644 app/javascript/flavours/glitch/features/direct_timeline/index.js create mode 100644 app/javascript/flavours/glitch/features/favourited_statuses/index.js create mode 100644 app/javascript/flavours/glitch/features/favourites/index.js create mode 100644 app/javascript/flavours/glitch/features/follow_requests/components/account_authorize.js create mode 100644 app/javascript/flavours/glitch/features/follow_requests/containers/account_authorize_container.js create mode 100644 app/javascript/flavours/glitch/features/follow_requests/index.js create mode 100644 app/javascript/flavours/glitch/features/followers/index.js create mode 100644 app/javascript/flavours/glitch/features/following/index.js create mode 100644 app/javascript/flavours/glitch/features/generic_not_found/index.js create mode 100644 app/javascript/flavours/glitch/features/getting_started/index.js create mode 100644 app/javascript/flavours/glitch/features/hashtag_timeline/index.js create mode 100644 app/javascript/flavours/glitch/features/home_timeline/components/column_settings.js create mode 100644 app/javascript/flavours/glitch/features/home_timeline/containers/column_settings_container.js create mode 100644 app/javascript/flavours/glitch/features/home_timeline/index.js create mode 100644 app/javascript/flavours/glitch/features/local_settings/index.js create mode 100644 app/javascript/flavours/glitch/features/local_settings/navigation/index.js create mode 100644 app/javascript/flavours/glitch/features/local_settings/navigation/item/index.js create mode 100644 app/javascript/flavours/glitch/features/local_settings/navigation/item/style.scss create mode 100644 app/javascript/flavours/glitch/features/local_settings/navigation/style.scss create mode 100644 app/javascript/flavours/glitch/features/local_settings/page/index.js create mode 100644 app/javascript/flavours/glitch/features/local_settings/page/item/index.js create mode 100644 app/javascript/flavours/glitch/features/local_settings/page/item/style.scss create mode 100644 app/javascript/flavours/glitch/features/local_settings/page/style.scss create mode 100644 app/javascript/flavours/glitch/features/local_settings/style.scss create mode 100644 app/javascript/flavours/glitch/features/mutes/index.js create mode 100644 app/javascript/flavours/glitch/features/notifications/components/clear_column_button.js create mode 100644 app/javascript/flavours/glitch/features/notifications/components/column_settings.js create mode 100644 app/javascript/flavours/glitch/features/notifications/components/follow.js create mode 100644 app/javascript/flavours/glitch/features/notifications/components/notification.js create mode 100644 app/javascript/flavours/glitch/features/notifications/components/overlay.js create mode 100644 app/javascript/flavours/glitch/features/notifications/components/setting_toggle.js create mode 100644 app/javascript/flavours/glitch/features/notifications/containers/column_settings_container.js create mode 100644 app/javascript/flavours/glitch/features/notifications/containers/notification_container.js create mode 100644 app/javascript/flavours/glitch/features/notifications/containers/overlay_container.js create mode 100644 app/javascript/flavours/glitch/features/notifications/index.js create mode 100644 app/javascript/flavours/glitch/features/pinned_statuses/index.js create mode 100644 app/javascript/flavours/glitch/features/public_timeline/containers/column_settings_container.js create mode 100644 app/javascript/flavours/glitch/features/public_timeline/index.js create mode 100644 app/javascript/flavours/glitch/features/reblogs/index.js create mode 100644 app/javascript/flavours/glitch/features/report/components/status_check_box.js create mode 100644 app/javascript/flavours/glitch/features/report/containers/status_check_box_container.js create mode 100644 app/javascript/flavours/glitch/features/standalone/compose/index.js create mode 100644 app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js create mode 100644 app/javascript/flavours/glitch/features/standalone/public_timeline/index.js create mode 100644 app/javascript/flavours/glitch/features/status/components/action_bar.js create mode 100644 app/javascript/flavours/glitch/features/status/components/card.js create mode 100644 app/javascript/flavours/glitch/features/status/components/detailed_status.js create mode 100644 app/javascript/flavours/glitch/features/status/containers/card_container.js create mode 100644 app/javascript/flavours/glitch/features/status/index.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/actions_modal.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/boost_modal.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/bundle.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/bundle_column_error.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/bundle_modal_error.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/column.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/column_header.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/column_link.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/column_loading.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/column_subheading.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/columns_area.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/confirmation_modal.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/doodle_modal.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/drawer_loading.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/embed_modal.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/image_loader.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/media_modal.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/modal_loading.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/modal_root.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/mute_modal.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/onboarding_modal.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/report_modal.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/tabs_bar.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/upload_area.js create mode 100644 app/javascript/flavours/glitch/features/ui/components/video_modal.js create mode 100644 app/javascript/flavours/glitch/features/ui/containers/bundle_container.js create mode 100644 app/javascript/flavours/glitch/features/ui/containers/columns_area_container.js create mode 100644 app/javascript/flavours/glitch/features/ui/containers/loading_bar_container.js create mode 100644 app/javascript/flavours/glitch/features/ui/containers/modal_container.js create mode 100644 app/javascript/flavours/glitch/features/ui/containers/notifications_container.js create mode 100644 app/javascript/flavours/glitch/features/ui/containers/status_list_container.js create mode 100644 app/javascript/flavours/glitch/features/ui/index.js create mode 100644 app/javascript/flavours/glitch/features/video/index.js create mode 100644 app/javascript/flavours/glitch/middleware/errors.js create mode 100644 app/javascript/flavours/glitch/middleware/loading_bar.js create mode 100644 app/javascript/flavours/glitch/middleware/sounds.js create mode 100644 app/javascript/flavours/glitch/packs/about.js create mode 100644 app/javascript/flavours/glitch/packs/common.js create mode 100644 app/javascript/flavours/glitch/packs/home.js create mode 100644 app/javascript/flavours/glitch/packs/public.js create mode 100644 app/javascript/flavours/glitch/packs/share.js create mode 100644 app/javascript/flavours/glitch/reducers/accounts.js create mode 100644 app/javascript/flavours/glitch/reducers/accounts_counters.js create mode 100644 app/javascript/flavours/glitch/reducers/alerts.js create mode 100644 app/javascript/flavours/glitch/reducers/cards.js create mode 100644 app/javascript/flavours/glitch/reducers/compose.js create mode 100644 app/javascript/flavours/glitch/reducers/contexts.js create mode 100644 app/javascript/flavours/glitch/reducers/custom_emojis.js create mode 100644 app/javascript/flavours/glitch/reducers/height_cache.js create mode 100644 app/javascript/flavours/glitch/reducers/index.js create mode 100644 app/javascript/flavours/glitch/reducers/local_settings.js create mode 100644 app/javascript/flavours/glitch/reducers/media_attachments.js create mode 100644 app/javascript/flavours/glitch/reducers/meta.js create mode 100644 app/javascript/flavours/glitch/reducers/modal.js create mode 100644 app/javascript/flavours/glitch/reducers/mutes.js create mode 100644 app/javascript/flavours/glitch/reducers/notifications.js create mode 100644 app/javascript/flavours/glitch/reducers/push_notifications.js create mode 100644 app/javascript/flavours/glitch/reducers/relationships.js create mode 100644 app/javascript/flavours/glitch/reducers/reports.js create mode 100644 app/javascript/flavours/glitch/reducers/search.js create mode 100644 app/javascript/flavours/glitch/reducers/settings.js create mode 100644 app/javascript/flavours/glitch/reducers/status_lists.js create mode 100644 app/javascript/flavours/glitch/reducers/statuses.js create mode 100644 app/javascript/flavours/glitch/reducers/timelines.js create mode 100644 app/javascript/flavours/glitch/reducers/user_lists.js create mode 100644 app/javascript/flavours/glitch/selectors/index.js create mode 100644 app/javascript/flavours/glitch/service_worker/entry.js create mode 100644 app/javascript/flavours/glitch/service_worker/web_push_notifications.js create mode 100644 app/javascript/flavours/glitch/store/configureStore.js create mode 100644 app/javascript/flavours/glitch/styles/_mixins.scss create mode 100644 app/javascript/flavours/glitch/styles/about.scss create mode 100644 app/javascript/flavours/glitch/styles/accounts.scss create mode 100644 app/javascript/flavours/glitch/styles/admin.scss create mode 100644 app/javascript/flavours/glitch/styles/basics.scss create mode 100644 app/javascript/flavours/glitch/styles/boost.scss create mode 100644 app/javascript/flavours/glitch/styles/compact_header.scss create mode 100644 app/javascript/flavours/glitch/styles/components.scss create mode 100644 app/javascript/flavours/glitch/styles/containers.scss create mode 100644 app/javascript/flavours/glitch/styles/doodle.scss create mode 100644 app/javascript/flavours/glitch/styles/emoji_picker.scss create mode 100644 app/javascript/flavours/glitch/styles/footer.scss create mode 100644 app/javascript/flavours/glitch/styles/forms.scss create mode 100644 app/javascript/flavours/glitch/styles/index.scss create mode 100644 app/javascript/flavours/glitch/styles/landing_strip.scss create mode 100644 app/javascript/flavours/glitch/styles/lists.scss create mode 100644 app/javascript/flavours/glitch/styles/reset copy.scss create mode 100644 app/javascript/flavours/glitch/styles/reset.scss create mode 100644 app/javascript/flavours/glitch/styles/rtl.scss create mode 100644 app/javascript/flavours/glitch/styles/stream_entries.scss create mode 100644 app/javascript/flavours/glitch/styles/tables.scss create mode 100644 app/javascript/flavours/glitch/styles/variables.scss create mode 100644 app/javascript/flavours/glitch/theme.yml create mode 100644 app/javascript/flavours/glitch/util/api.js create mode 100644 app/javascript/flavours/glitch/util/async-components.js create mode 100644 app/javascript/flavours/glitch/util/base_polyfills.js create mode 100644 app/javascript/flavours/glitch/util/bio_metadata.js create mode 100644 app/javascript/flavours/glitch/util/counter.js create mode 100644 app/javascript/flavours/glitch/util/emoji/emoji_compressed.js create mode 100644 app/javascript/flavours/glitch/util/emoji/emoji_map.json create mode 100644 app/javascript/flavours/glitch/util/emoji/emoji_mart_data_light.js create mode 100644 app/javascript/flavours/glitch/util/emoji/emoji_mart_search_light.js create mode 100644 app/javascript/flavours/glitch/util/emoji/emoji_picker.js create mode 100644 app/javascript/flavours/glitch/util/emoji/emoji_unicode_mapping_light.js create mode 100644 app/javascript/flavours/glitch/util/emoji/emoji_utils.js create mode 100644 app/javascript/flavours/glitch/util/emoji/index.js create mode 100644 app/javascript/flavours/glitch/util/emoji/unicode_to_filename.js create mode 100644 app/javascript/flavours/glitch/util/emoji/unicode_to_unified_name.js create mode 100644 app/javascript/flavours/glitch/util/extra_polyfills.js create mode 100644 app/javascript/flavours/glitch/util/fullscreen.js create mode 100644 app/javascript/flavours/glitch/util/get_rect_from_entry.js create mode 100644 app/javascript/flavours/glitch/util/initial_state.js create mode 100644 app/javascript/flavours/glitch/util/intersection_observer_wrapper.js create mode 100644 app/javascript/flavours/glitch/util/is_mobile.js create mode 100644 app/javascript/flavours/glitch/util/link_header.js create mode 100644 app/javascript/flavours/glitch/util/load_polyfills.js create mode 100644 app/javascript/flavours/glitch/util/main.js create mode 100644 app/javascript/flavours/glitch/util/optional_motion.js create mode 100644 app/javascript/flavours/glitch/util/performance.js create mode 100644 app/javascript/flavours/glitch/util/react_router_helpers.js create mode 100644 app/javascript/flavours/glitch/util/ready.js create mode 100644 app/javascript/flavours/glitch/util/reduced_motion.js create mode 100644 app/javascript/flavours/glitch/util/rtl.js create mode 100644 app/javascript/flavours/glitch/util/schedule_idle_task.js create mode 100644 app/javascript/flavours/glitch/util/scroll.js create mode 100644 app/javascript/flavours/glitch/util/stream.js create mode 100644 app/javascript/flavours/glitch/util/url_regex.js create mode 100644 app/javascript/flavours/glitch/util/uuid.js create mode 100644 app/javascript/flavours/glitch/util/web_push_subscription.js create mode 100644 app/javascript/flavours/vanilla/theme.yml delete mode 100644 app/javascript/themes/glitch/actions/accounts.js delete mode 100644 app/javascript/themes/glitch/actions/alerts.js delete mode 100644 app/javascript/themes/glitch/actions/blocks.js delete mode 100644 app/javascript/themes/glitch/actions/bundles.js delete mode 100644 app/javascript/themes/glitch/actions/cards.js delete mode 100644 app/javascript/themes/glitch/actions/columns.js delete mode 100644 app/javascript/themes/glitch/actions/compose.js delete mode 100644 app/javascript/themes/glitch/actions/domain_blocks.js delete mode 100644 app/javascript/themes/glitch/actions/emojis.js delete mode 100644 app/javascript/themes/glitch/actions/favourites.js delete mode 100644 app/javascript/themes/glitch/actions/height_cache.js delete mode 100644 app/javascript/themes/glitch/actions/interactions.js delete mode 100644 app/javascript/themes/glitch/actions/local_settings.js delete mode 100644 app/javascript/themes/glitch/actions/modal.js delete mode 100644 app/javascript/themes/glitch/actions/mutes.js delete mode 100644 app/javascript/themes/glitch/actions/notifications.js delete mode 100644 app/javascript/themes/glitch/actions/onboarding.js delete mode 100644 app/javascript/themes/glitch/actions/pin_statuses.js delete mode 100644 app/javascript/themes/glitch/actions/push_notifications.js delete mode 100644 app/javascript/themes/glitch/actions/reports.js delete mode 100644 app/javascript/themes/glitch/actions/search.js delete mode 100644 app/javascript/themes/glitch/actions/settings.js delete mode 100644 app/javascript/themes/glitch/actions/statuses.js delete mode 100644 app/javascript/themes/glitch/actions/store.js delete mode 100644 app/javascript/themes/glitch/actions/streaming.js delete mode 100644 app/javascript/themes/glitch/actions/timelines.js delete mode 100644 app/javascript/themes/glitch/components/account.js delete mode 100644 app/javascript/themes/glitch/components/attachment_list.js delete mode 100644 app/javascript/themes/glitch/components/autosuggest_emoji.js delete mode 100644 app/javascript/themes/glitch/components/autosuggest_textarea.js delete mode 100644 app/javascript/themes/glitch/components/avatar.js delete mode 100644 app/javascript/themes/glitch/components/avatar_overlay.js delete mode 100644 app/javascript/themes/glitch/components/button.js delete mode 100644 app/javascript/themes/glitch/components/collapsable.js delete mode 100644 app/javascript/themes/glitch/components/column.js delete mode 100644 app/javascript/themes/glitch/components/column_back_button.js delete mode 100644 app/javascript/themes/glitch/components/column_back_button_slim.js delete mode 100644 app/javascript/themes/glitch/components/column_header.js delete mode 100644 app/javascript/themes/glitch/components/display_name.js delete mode 100644 app/javascript/themes/glitch/components/dropdown_menu.js delete mode 100644 app/javascript/themes/glitch/components/extended_video_player.js delete mode 100644 app/javascript/themes/glitch/components/icon_button.js delete mode 100644 app/javascript/themes/glitch/components/intersection_observer_article.js delete mode 100644 app/javascript/themes/glitch/components/load_more.js delete mode 100644 app/javascript/themes/glitch/components/loading_indicator.js delete mode 100644 app/javascript/themes/glitch/components/media_gallery.js delete mode 100644 app/javascript/themes/glitch/components/missing_indicator.js delete mode 100644 app/javascript/themes/glitch/components/notification_purge_buttons.js delete mode 100644 app/javascript/themes/glitch/components/permalink.js delete mode 100644 app/javascript/themes/glitch/components/relative_timestamp.js delete mode 100644 app/javascript/themes/glitch/components/scrollable_list.js delete mode 100644 app/javascript/themes/glitch/components/setting_text.js delete mode 100644 app/javascript/themes/glitch/components/status.js delete mode 100644 app/javascript/themes/glitch/components/status_action_bar.js delete mode 100644 app/javascript/themes/glitch/components/status_content.js delete mode 100644 app/javascript/themes/glitch/components/status_header.js delete mode 100644 app/javascript/themes/glitch/components/status_list.js delete mode 100644 app/javascript/themes/glitch/components/status_prepend.js delete mode 100644 app/javascript/themes/glitch/components/status_visibility_icon.js delete mode 100644 app/javascript/themes/glitch/containers/account_container.js delete mode 100644 app/javascript/themes/glitch/containers/card_container.js delete mode 100644 app/javascript/themes/glitch/containers/compose_container.js delete mode 100644 app/javascript/themes/glitch/containers/dropdown_menu_container.js delete mode 100644 app/javascript/themes/glitch/containers/intersection_observer_article_container.js delete mode 100644 app/javascript/themes/glitch/containers/mastodon.js delete mode 100644 app/javascript/themes/glitch/containers/media_gallery_container.js delete mode 100644 app/javascript/themes/glitch/containers/notification_purge_buttons_container.js delete mode 100644 app/javascript/themes/glitch/containers/status_container.js delete mode 100644 app/javascript/themes/glitch/containers/timeline_container.js delete mode 100644 app/javascript/themes/glitch/containers/video_container.js delete mode 100644 app/javascript/themes/glitch/features/account/components/action_bar.js delete mode 100644 app/javascript/themes/glitch/features/account/components/header.js delete mode 100644 app/javascript/themes/glitch/features/account_gallery/components/media_item.js delete mode 100644 app/javascript/themes/glitch/features/account_gallery/index.js delete mode 100644 app/javascript/themes/glitch/features/account_timeline/components/header.js delete mode 100644 app/javascript/themes/glitch/features/account_timeline/containers/header_container.js delete mode 100644 app/javascript/themes/glitch/features/account_timeline/index.js delete mode 100644 app/javascript/themes/glitch/features/blocks/index.js delete mode 100644 app/javascript/themes/glitch/features/community_timeline/components/column_settings.js delete mode 100644 app/javascript/themes/glitch/features/community_timeline/containers/column_settings_container.js delete mode 100644 app/javascript/themes/glitch/features/community_timeline/index.js delete mode 100644 app/javascript/themes/glitch/features/compose/components/advanced_options.js delete mode 100644 app/javascript/themes/glitch/features/compose/components/advanced_options_toggle.js delete mode 100644 app/javascript/themes/glitch/features/compose/components/attach_options.js delete mode 100644 app/javascript/themes/glitch/features/compose/components/autosuggest_account.js delete mode 100644 app/javascript/themes/glitch/features/compose/components/character_counter.js delete mode 100644 app/javascript/themes/glitch/features/compose/components/compose_form.js delete mode 100644 app/javascript/themes/glitch/features/compose/components/dropdown.js delete mode 100644 app/javascript/themes/glitch/features/compose/components/emoji_picker_dropdown.js delete mode 100644 app/javascript/themes/glitch/features/compose/components/navigation_bar.js delete mode 100644 app/javascript/themes/glitch/features/compose/components/privacy_dropdown.js delete mode 100644 app/javascript/themes/glitch/features/compose/components/reply_indicator.js delete mode 100644 app/javascript/themes/glitch/features/compose/components/search.js delete mode 100644 app/javascript/themes/glitch/features/compose/components/search_results.js delete mode 100644 app/javascript/themes/glitch/features/compose/components/text_icon_button.js delete mode 100644 app/javascript/themes/glitch/features/compose/components/upload.js delete mode 100644 app/javascript/themes/glitch/features/compose/components/upload_button.js delete mode 100644 app/javascript/themes/glitch/features/compose/components/upload_form.js delete mode 100644 app/javascript/themes/glitch/features/compose/components/upload_progress.js delete mode 100644 app/javascript/themes/glitch/features/compose/components/warning.js delete mode 100644 app/javascript/themes/glitch/features/compose/containers/advanced_options_container.js delete mode 100644 app/javascript/themes/glitch/features/compose/containers/autosuggest_account_container.js delete mode 100644 app/javascript/themes/glitch/features/compose/containers/compose_form_container.js delete mode 100644 app/javascript/themes/glitch/features/compose/containers/emoji_picker_dropdown_container.js delete mode 100644 app/javascript/themes/glitch/features/compose/containers/navigation_container.js delete mode 100644 app/javascript/themes/glitch/features/compose/containers/privacy_dropdown_container.js delete mode 100644 app/javascript/themes/glitch/features/compose/containers/reply_indicator_container.js delete mode 100644 app/javascript/themes/glitch/features/compose/containers/search_container.js delete mode 100644 app/javascript/themes/glitch/features/compose/containers/search_results_container.js delete mode 100644 app/javascript/themes/glitch/features/compose/containers/sensitive_button_container.js delete mode 100644 app/javascript/themes/glitch/features/compose/containers/spoiler_button_container.js delete mode 100644 app/javascript/themes/glitch/features/compose/containers/upload_button_container.js delete mode 100644 app/javascript/themes/glitch/features/compose/containers/upload_container.js delete mode 100644 app/javascript/themes/glitch/features/compose/containers/upload_form_container.js delete mode 100644 app/javascript/themes/glitch/features/compose/containers/upload_progress_container.js delete mode 100644 app/javascript/themes/glitch/features/compose/containers/warning_container.js delete mode 100644 app/javascript/themes/glitch/features/compose/index.js delete mode 100644 app/javascript/themes/glitch/features/direct_timeline/containers/column_settings_container.js delete mode 100644 app/javascript/themes/glitch/features/direct_timeline/index.js delete mode 100644 app/javascript/themes/glitch/features/favourited_statuses/index.js delete mode 100644 app/javascript/themes/glitch/features/favourites/index.js delete mode 100644 app/javascript/themes/glitch/features/follow_requests/components/account_authorize.js delete mode 100644 app/javascript/themes/glitch/features/follow_requests/containers/account_authorize_container.js delete mode 100644 app/javascript/themes/glitch/features/follow_requests/index.js delete mode 100644 app/javascript/themes/glitch/features/followers/index.js delete mode 100644 app/javascript/themes/glitch/features/following/index.js delete mode 100644 app/javascript/themes/glitch/features/generic_not_found/index.js delete mode 100644 app/javascript/themes/glitch/features/getting_started/index.js delete mode 100644 app/javascript/themes/glitch/features/hashtag_timeline/index.js delete mode 100644 app/javascript/themes/glitch/features/home_timeline/components/column_settings.js delete mode 100644 app/javascript/themes/glitch/features/home_timeline/containers/column_settings_container.js delete mode 100644 app/javascript/themes/glitch/features/home_timeline/index.js delete mode 100644 app/javascript/themes/glitch/features/local_settings/index.js delete mode 100644 app/javascript/themes/glitch/features/local_settings/navigation/index.js delete mode 100644 app/javascript/themes/glitch/features/local_settings/navigation/item/index.js delete mode 100644 app/javascript/themes/glitch/features/local_settings/navigation/item/style.scss delete mode 100644 app/javascript/themes/glitch/features/local_settings/navigation/style.scss delete mode 100644 app/javascript/themes/glitch/features/local_settings/page/index.js delete mode 100644 app/javascript/themes/glitch/features/local_settings/page/item/index.js delete mode 100644 app/javascript/themes/glitch/features/local_settings/page/item/style.scss delete mode 100644 app/javascript/themes/glitch/features/local_settings/page/style.scss delete mode 100644 app/javascript/themes/glitch/features/local_settings/style.scss delete mode 100644 app/javascript/themes/glitch/features/mutes/index.js delete mode 100644 app/javascript/themes/glitch/features/notifications/components/clear_column_button.js delete mode 100644 app/javascript/themes/glitch/features/notifications/components/column_settings.js delete mode 100644 app/javascript/themes/glitch/features/notifications/components/follow.js delete mode 100644 app/javascript/themes/glitch/features/notifications/components/notification.js delete mode 100644 app/javascript/themes/glitch/features/notifications/components/overlay.js delete mode 100644 app/javascript/themes/glitch/features/notifications/components/setting_toggle.js delete mode 100644 app/javascript/themes/glitch/features/notifications/containers/column_settings_container.js delete mode 100644 app/javascript/themes/glitch/features/notifications/containers/notification_container.js delete mode 100644 app/javascript/themes/glitch/features/notifications/containers/overlay_container.js delete mode 100644 app/javascript/themes/glitch/features/notifications/index.js delete mode 100644 app/javascript/themes/glitch/features/pinned_statuses/index.js delete mode 100644 app/javascript/themes/glitch/features/public_timeline/containers/column_settings_container.js delete mode 100644 app/javascript/themes/glitch/features/public_timeline/index.js delete mode 100644 app/javascript/themes/glitch/features/reblogs/index.js delete mode 100644 app/javascript/themes/glitch/features/report/components/status_check_box.js delete mode 100644 app/javascript/themes/glitch/features/report/containers/status_check_box_container.js delete mode 100644 app/javascript/themes/glitch/features/standalone/compose/index.js delete mode 100644 app/javascript/themes/glitch/features/standalone/hashtag_timeline/index.js delete mode 100644 app/javascript/themes/glitch/features/standalone/public_timeline/index.js delete mode 100644 app/javascript/themes/glitch/features/status/components/action_bar.js delete mode 100644 app/javascript/themes/glitch/features/status/components/card.js delete mode 100644 app/javascript/themes/glitch/features/status/components/detailed_status.js delete mode 100644 app/javascript/themes/glitch/features/status/containers/card_container.js delete mode 100644 app/javascript/themes/glitch/features/status/index.js delete mode 100644 app/javascript/themes/glitch/features/ui/components/actions_modal.js delete mode 100644 app/javascript/themes/glitch/features/ui/components/boost_modal.js delete mode 100644 app/javascript/themes/glitch/features/ui/components/bundle.js delete mode 100644 app/javascript/themes/glitch/features/ui/components/bundle_column_error.js delete mode 100644 app/javascript/themes/glitch/features/ui/components/bundle_modal_error.js delete mode 100644 app/javascript/themes/glitch/features/ui/components/column.js delete mode 100644 app/javascript/themes/glitch/features/ui/components/column_header.js delete mode 100644 app/javascript/themes/glitch/features/ui/components/column_link.js delete mode 100644 app/javascript/themes/glitch/features/ui/components/column_loading.js delete mode 100644 app/javascript/themes/glitch/features/ui/components/column_subheading.js delete mode 100644 app/javascript/themes/glitch/features/ui/components/columns_area.js delete mode 100644 app/javascript/themes/glitch/features/ui/components/confirmation_modal.js delete mode 100644 app/javascript/themes/glitch/features/ui/components/doodle_modal.js delete mode 100644 app/javascript/themes/glitch/features/ui/components/drawer_loading.js delete mode 100644 app/javascript/themes/glitch/features/ui/components/embed_modal.js delete mode 100644 app/javascript/themes/glitch/features/ui/components/image_loader.js delete mode 100644 app/javascript/themes/glitch/features/ui/components/media_modal.js delete mode 100644 app/javascript/themes/glitch/features/ui/components/modal_loading.js delete mode 100644 app/javascript/themes/glitch/features/ui/components/modal_root.js delete mode 100644 app/javascript/themes/glitch/features/ui/components/mute_modal.js delete mode 100644 app/javascript/themes/glitch/features/ui/components/onboarding_modal.js delete mode 100644 app/javascript/themes/glitch/features/ui/components/report_modal.js delete mode 100644 app/javascript/themes/glitch/features/ui/components/tabs_bar.js delete mode 100644 app/javascript/themes/glitch/features/ui/components/upload_area.js delete mode 100644 app/javascript/themes/glitch/features/ui/components/video_modal.js delete mode 100644 app/javascript/themes/glitch/features/ui/containers/bundle_container.js delete mode 100644 app/javascript/themes/glitch/features/ui/containers/columns_area_container.js delete mode 100644 app/javascript/themes/glitch/features/ui/containers/loading_bar_container.js delete mode 100644 app/javascript/themes/glitch/features/ui/containers/modal_container.js delete mode 100644 app/javascript/themes/glitch/features/ui/containers/notifications_container.js delete mode 100644 app/javascript/themes/glitch/features/ui/containers/status_list_container.js delete mode 100644 app/javascript/themes/glitch/features/ui/index.js delete mode 100644 app/javascript/themes/glitch/features/video/index.js delete mode 100644 app/javascript/themes/glitch/middleware/errors.js delete mode 100644 app/javascript/themes/glitch/middleware/loading_bar.js delete mode 100644 app/javascript/themes/glitch/middleware/sounds.js delete mode 100644 app/javascript/themes/glitch/packs/about.js delete mode 100644 app/javascript/themes/glitch/packs/common.js delete mode 100644 app/javascript/themes/glitch/packs/home.js delete mode 100644 app/javascript/themes/glitch/packs/public.js delete mode 100644 app/javascript/themes/glitch/packs/share.js delete mode 100644 app/javascript/themes/glitch/reducers/accounts.js delete mode 100644 app/javascript/themes/glitch/reducers/accounts_counters.js delete mode 100644 app/javascript/themes/glitch/reducers/alerts.js delete mode 100644 app/javascript/themes/glitch/reducers/cards.js delete mode 100644 app/javascript/themes/glitch/reducers/compose.js delete mode 100644 app/javascript/themes/glitch/reducers/contexts.js delete mode 100644 app/javascript/themes/glitch/reducers/custom_emojis.js delete mode 100644 app/javascript/themes/glitch/reducers/height_cache.js delete mode 100644 app/javascript/themes/glitch/reducers/index.js delete mode 100644 app/javascript/themes/glitch/reducers/local_settings.js delete mode 100644 app/javascript/themes/glitch/reducers/media_attachments.js delete mode 100644 app/javascript/themes/glitch/reducers/meta.js delete mode 100644 app/javascript/themes/glitch/reducers/modal.js delete mode 100644 app/javascript/themes/glitch/reducers/mutes.js delete mode 100644 app/javascript/themes/glitch/reducers/notifications.js delete mode 100644 app/javascript/themes/glitch/reducers/push_notifications.js delete mode 100644 app/javascript/themes/glitch/reducers/relationships.js delete mode 100644 app/javascript/themes/glitch/reducers/reports.js delete mode 100644 app/javascript/themes/glitch/reducers/search.js delete mode 100644 app/javascript/themes/glitch/reducers/settings.js delete mode 100644 app/javascript/themes/glitch/reducers/status_lists.js delete mode 100644 app/javascript/themes/glitch/reducers/statuses.js delete mode 100644 app/javascript/themes/glitch/reducers/timelines.js delete mode 100644 app/javascript/themes/glitch/reducers/user_lists.js delete mode 100644 app/javascript/themes/glitch/selectors/index.js delete mode 100644 app/javascript/themes/glitch/service_worker/entry.js delete mode 100644 app/javascript/themes/glitch/service_worker/web_push_notifications.js delete mode 100644 app/javascript/themes/glitch/store/configureStore.js delete mode 100644 app/javascript/themes/glitch/styles/_mixins.scss delete mode 100644 app/javascript/themes/glitch/styles/about.scss delete mode 100644 app/javascript/themes/glitch/styles/accounts.scss delete mode 100644 app/javascript/themes/glitch/styles/admin.scss delete mode 100644 app/javascript/themes/glitch/styles/basics.scss delete mode 100644 app/javascript/themes/glitch/styles/boost.scss delete mode 100644 app/javascript/themes/glitch/styles/compact_header.scss delete mode 100644 app/javascript/themes/glitch/styles/components.scss delete mode 100644 app/javascript/themes/glitch/styles/containers.scss delete mode 100644 app/javascript/themes/glitch/styles/doodle.scss delete mode 100644 app/javascript/themes/glitch/styles/emoji_picker.scss delete mode 100644 app/javascript/themes/glitch/styles/footer.scss delete mode 100644 app/javascript/themes/glitch/styles/forms.scss delete mode 100644 app/javascript/themes/glitch/styles/index.scss delete mode 100644 app/javascript/themes/glitch/styles/landing_strip.scss delete mode 100644 app/javascript/themes/glitch/styles/lists.scss delete mode 100644 app/javascript/themes/glitch/styles/reset copy.scss delete mode 100644 app/javascript/themes/glitch/styles/reset.scss delete mode 100644 app/javascript/themes/glitch/styles/rtl.scss delete mode 100644 app/javascript/themes/glitch/styles/stream_entries.scss delete mode 100644 app/javascript/themes/glitch/styles/tables.scss delete mode 100644 app/javascript/themes/glitch/styles/variables.scss delete mode 100644 app/javascript/themes/glitch/theme.yml delete mode 100644 app/javascript/themes/glitch/util/api.js delete mode 100644 app/javascript/themes/glitch/util/async-components.js delete mode 100644 app/javascript/themes/glitch/util/base_polyfills.js delete mode 100644 app/javascript/themes/glitch/util/bio_metadata.js delete mode 100644 app/javascript/themes/glitch/util/counter.js delete mode 100644 app/javascript/themes/glitch/util/emoji/emoji_compressed.js delete mode 100644 app/javascript/themes/glitch/util/emoji/emoji_map.json delete mode 100644 app/javascript/themes/glitch/util/emoji/emoji_mart_data_light.js delete mode 100644 app/javascript/themes/glitch/util/emoji/emoji_mart_search_light.js delete mode 100644 app/javascript/themes/glitch/util/emoji/emoji_picker.js delete mode 100644 app/javascript/themes/glitch/util/emoji/emoji_unicode_mapping_light.js delete mode 100644 app/javascript/themes/glitch/util/emoji/emoji_utils.js delete mode 100644 app/javascript/themes/glitch/util/emoji/index.js delete mode 100644 app/javascript/themes/glitch/util/emoji/unicode_to_filename.js delete mode 100644 app/javascript/themes/glitch/util/emoji/unicode_to_unified_name.js delete mode 100644 app/javascript/themes/glitch/util/extra_polyfills.js delete mode 100644 app/javascript/themes/glitch/util/fullscreen.js delete mode 100644 app/javascript/themes/glitch/util/get_rect_from_entry.js delete mode 100644 app/javascript/themes/glitch/util/initial_state.js delete mode 100644 app/javascript/themes/glitch/util/intersection_observer_wrapper.js delete mode 100644 app/javascript/themes/glitch/util/is_mobile.js delete mode 100644 app/javascript/themes/glitch/util/link_header.js delete mode 100644 app/javascript/themes/glitch/util/load_polyfills.js delete mode 100644 app/javascript/themes/glitch/util/main.js delete mode 100644 app/javascript/themes/glitch/util/optional_motion.js delete mode 100644 app/javascript/themes/glitch/util/performance.js delete mode 100644 app/javascript/themes/glitch/util/react_router_helpers.js delete mode 100644 app/javascript/themes/glitch/util/ready.js delete mode 100644 app/javascript/themes/glitch/util/reduced_motion.js delete mode 100644 app/javascript/themes/glitch/util/rtl.js delete mode 100644 app/javascript/themes/glitch/util/schedule_idle_task.js delete mode 100644 app/javascript/themes/glitch/util/scroll.js delete mode 100644 app/javascript/themes/glitch/util/stream.js delete mode 100644 app/javascript/themes/glitch/util/url_regex.js delete mode 100644 app/javascript/themes/glitch/util/uuid.js delete mode 100644 app/javascript/themes/glitch/util/web_push_subscription.js delete mode 160000 app/javascript/themes/mastodon-go delete mode 100644 app/javascript/themes/vanilla/theme.yml (limited to 'app/javascript/core') diff --git a/.gitmodules b/.gitmodules index 35b0cd787..e69de29bb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +0,0 @@ -[submodule "app/javascript/themes/mastodon-go"] - path = app/javascript/themes/mastodon-go - url = https://github.com/marrus-sh/mastodon-go diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index f5753963d..d116c4767 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -12,7 +12,7 @@ class ApplicationController < ActionController::Base helper_method :current_account helper_method :current_session - helper_method :current_theme + helper_method :current_flavour helper_method :current_skin helper_method :single_user_mode? @@ -57,8 +57,8 @@ class ApplicationController < ActionController::Base def pack(data, pack_name, skin = 'default') return nil unless pack?(data, pack_name) pack_data = { - common: pack_name == 'common' ? nil : resolve_pack(data['name'] ? Themes.instance.get(current_theme) : Themes.instance.core, 'common'), - name: data['name'], + common: pack_name == 'common' ? nil : resolve_pack(data['name'] ? Themes.instance.flavour(current_flavour) : Themes.instance.core, 'common'), + flavour: data['name'], pack: pack_name, preload: nil, skin: nil, @@ -88,8 +88,8 @@ class ApplicationController < ActionController::Base def nil_pack(data, pack_name, skin = 'default') { - common: pack_name == 'common' ? nil : resolve_pack(data['name'] ? Themes.instance.get(current_theme) : Themes.instance.core, 'common', skin), - name: data['name'], + common: pack_name == 'common' ? nil : resolve_pack(data['name'] ? Themes.instance.flavour(current_flavour) : Themes.instance.core, 'common', skin), + flavour: data['name'], pack: nil, preload: nil, skin: nil, @@ -102,23 +102,23 @@ class ApplicationController < ActionController::Base if data['name'] && data.key?('fallback') if data['fallback'].nil? return nil_pack(data, pack_name, skin) - elsif data['fallback'].is_a?(String) && Themes.instance.get(data['fallback']) - return resolve_pack(Themes.instance.get(data['fallback']), pack_name, skin) + elsif data['fallback'].is_a?(String) && Themes.instance.flavour(data['fallback']) + return resolve_pack(Themes.instance.flavour(data['fallback']), pack_name, skin) elsif data['fallback'].is_a?(Array) data['fallback'].each do |fallback| - return resolve_pack(Themes.instance.get(fallback), pack_name, skin) if Themes.instance.get(fallback) + return resolve_pack(Themes.instance.flavour(fallback), pack_name, skin) if Themes.instance.flavour(fallback) end end return nil_pack(data, pack_name, skin) end - return data.key?('name') && data['name'] != default_theme ? resolve_pack(Themes.instance.get(default_theme), pack_name, skin) : nil_pack(data, pack_name, skin) + return data.key?('name') && data['name'] != Setting.default_settings['flavour'] ? resolve_pack(Themes.instance.flavour(Setting.default_settings['flavour']), pack_name, skin) : nil_pack(data, pack_name, skin) end result end def use_pack(pack_name) @core = resolve_pack(Themes.instance.core, pack_name) - @theme = resolve_pack(Themes.instance.get(current_theme), pack_name, current_skin) + @theme = resolve_pack(Themes.instance.flavour(current_flavour), pack_name, current_skin) end protected @@ -151,21 +151,13 @@ class ApplicationController < ActionController::Base @current_session ||= SessionActivation.find_by(session_id: cookies.signed['_session_id']) end - def default_theme - Setting.default_settings['theme'] - end - - def current_theme - return default_theme unless Themes.instance.names.include? current_user&.setting_theme - current_user.setting_theme - end - - def default_skin - 'default' + def current_flavour + return Setting.default_settings['flavour'] unless Themes.instance.flavours.include? current_user&.setting_flavour + current_user.setting_flavour end def current_skin - return default_skin unless Themes.instance.skins_for(current_theme).include? current_user&.setting_skin + return 'default' unless Themes.instance.skins_for(current_flavour).include? current_user&.setting_skin current_user.setting_skin end diff --git a/app/javascript/core/settings.js b/app/javascript/core/settings.js index bc5f9ed1d..1e4bb4ced 100644 --- a/app/javascript/core/settings.js +++ b/app/javascript/core/settings.js @@ -3,7 +3,7 @@ const { length } = require('stringz'); const { delegate } = require('rails-ujs'); -import { processBio } from 'themes/glitch/util/bio_metadata'; +import { processBio } from 'flavours/glitch/util/bio_metadata'; delegate(document, '.account_display_name', 'input', ({ target }) => { const nameCounter = document.querySelector('.name-counter'); diff --git a/app/javascript/flavours/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js new file mode 100644 index 000000000..8ab92f9e7 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/accounts.js @@ -0,0 +1,661 @@ +import api, { getLinks } from 'flavours/glitch/util/api'; + +export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST'; +export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS'; +export const ACCOUNT_FETCH_FAIL = 'ACCOUNT_FETCH_FAIL'; + +export const ACCOUNT_FOLLOW_REQUEST = 'ACCOUNT_FOLLOW_REQUEST'; +export const ACCOUNT_FOLLOW_SUCCESS = 'ACCOUNT_FOLLOW_SUCCESS'; +export const ACCOUNT_FOLLOW_FAIL = 'ACCOUNT_FOLLOW_FAIL'; + +export const ACCOUNT_UNFOLLOW_REQUEST = 'ACCOUNT_UNFOLLOW_REQUEST'; +export const ACCOUNT_UNFOLLOW_SUCCESS = 'ACCOUNT_UNFOLLOW_SUCCESS'; +export const ACCOUNT_UNFOLLOW_FAIL = 'ACCOUNT_UNFOLLOW_FAIL'; + +export const ACCOUNT_BLOCK_REQUEST = 'ACCOUNT_BLOCK_REQUEST'; +export const ACCOUNT_BLOCK_SUCCESS = 'ACCOUNT_BLOCK_SUCCESS'; +export const ACCOUNT_BLOCK_FAIL = 'ACCOUNT_BLOCK_FAIL'; + +export const ACCOUNT_UNBLOCK_REQUEST = 'ACCOUNT_UNBLOCK_REQUEST'; +export const ACCOUNT_UNBLOCK_SUCCESS = 'ACCOUNT_UNBLOCK_SUCCESS'; +export const ACCOUNT_UNBLOCK_FAIL = 'ACCOUNT_UNBLOCK_FAIL'; + +export const ACCOUNT_MUTE_REQUEST = 'ACCOUNT_MUTE_REQUEST'; +export const ACCOUNT_MUTE_SUCCESS = 'ACCOUNT_MUTE_SUCCESS'; +export const ACCOUNT_MUTE_FAIL = 'ACCOUNT_MUTE_FAIL'; + +export const ACCOUNT_UNMUTE_REQUEST = 'ACCOUNT_UNMUTE_REQUEST'; +export const ACCOUNT_UNMUTE_SUCCESS = 'ACCOUNT_UNMUTE_SUCCESS'; +export const ACCOUNT_UNMUTE_FAIL = 'ACCOUNT_UNMUTE_FAIL'; + +export const FOLLOWERS_FETCH_REQUEST = 'FOLLOWERS_FETCH_REQUEST'; +export const FOLLOWERS_FETCH_SUCCESS = 'FOLLOWERS_FETCH_SUCCESS'; +export const FOLLOWERS_FETCH_FAIL = 'FOLLOWERS_FETCH_FAIL'; + +export const FOLLOWERS_EXPAND_REQUEST = 'FOLLOWERS_EXPAND_REQUEST'; +export const FOLLOWERS_EXPAND_SUCCESS = 'FOLLOWERS_EXPAND_SUCCESS'; +export const FOLLOWERS_EXPAND_FAIL = 'FOLLOWERS_EXPAND_FAIL'; + +export const FOLLOWING_FETCH_REQUEST = 'FOLLOWING_FETCH_REQUEST'; +export const FOLLOWING_FETCH_SUCCESS = 'FOLLOWING_FETCH_SUCCESS'; +export const FOLLOWING_FETCH_FAIL = 'FOLLOWING_FETCH_FAIL'; + +export const FOLLOWING_EXPAND_REQUEST = 'FOLLOWING_EXPAND_REQUEST'; +export const FOLLOWING_EXPAND_SUCCESS = 'FOLLOWING_EXPAND_SUCCESS'; +export const FOLLOWING_EXPAND_FAIL = 'FOLLOWING_EXPAND_FAIL'; + +export const RELATIONSHIPS_FETCH_REQUEST = 'RELATIONSHIPS_FETCH_REQUEST'; +export const RELATIONSHIPS_FETCH_SUCCESS = 'RELATIONSHIPS_FETCH_SUCCESS'; +export const RELATIONSHIPS_FETCH_FAIL = 'RELATIONSHIPS_FETCH_FAIL'; + +export const FOLLOW_REQUESTS_FETCH_REQUEST = 'FOLLOW_REQUESTS_FETCH_REQUEST'; +export const FOLLOW_REQUESTS_FETCH_SUCCESS = 'FOLLOW_REQUESTS_FETCH_SUCCESS'; +export const FOLLOW_REQUESTS_FETCH_FAIL = 'FOLLOW_REQUESTS_FETCH_FAIL'; + +export const FOLLOW_REQUESTS_EXPAND_REQUEST = 'FOLLOW_REQUESTS_EXPAND_REQUEST'; +export const FOLLOW_REQUESTS_EXPAND_SUCCESS = 'FOLLOW_REQUESTS_EXPAND_SUCCESS'; +export const FOLLOW_REQUESTS_EXPAND_FAIL = 'FOLLOW_REQUESTS_EXPAND_FAIL'; + +export const FOLLOW_REQUEST_AUTHORIZE_REQUEST = 'FOLLOW_REQUEST_AUTHORIZE_REQUEST'; +export const FOLLOW_REQUEST_AUTHORIZE_SUCCESS = 'FOLLOW_REQUEST_AUTHORIZE_SUCCESS'; +export const FOLLOW_REQUEST_AUTHORIZE_FAIL = 'FOLLOW_REQUEST_AUTHORIZE_FAIL'; + +export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST'; +export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS'; +export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL'; + +export function fetchAccount(id) { + return (dispatch, getState) => { + dispatch(fetchRelationships([id])); + + if (getState().getIn(['accounts', id], null) !== null) { + return; + } + + dispatch(fetchAccountRequest(id)); + + api(getState).get(`/api/v1/accounts/${id}`).then(response => { + dispatch(fetchAccountSuccess(response.data)); + }).catch(error => { + dispatch(fetchAccountFail(id, error)); + }); + }; +}; + +export function fetchAccountRequest(id) { + return { + type: ACCOUNT_FETCH_REQUEST, + id, + }; +}; + +export function fetchAccountSuccess(account) { + return { + type: ACCOUNT_FETCH_SUCCESS, + account, + }; +}; + +export function fetchAccountFail(id, error) { + return { + type: ACCOUNT_FETCH_FAIL, + id, + error, + skipAlert: true, + }; +}; + +export function followAccount(id, reblogs = true) { + return (dispatch, getState) => { + const alreadyFollowing = getState().getIn(['relationships', id, 'following']); + dispatch(followAccountRequest(id)); + + api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => { + dispatch(followAccountSuccess(response.data, alreadyFollowing)); + }).catch(error => { + dispatch(followAccountFail(error)); + }); + }; +}; + +export function unfollowAccount(id) { + return (dispatch, getState) => { + dispatch(unfollowAccountRequest(id)); + + api(getState).post(`/api/v1/accounts/${id}/unfollow`).then(response => { + dispatch(unfollowAccountSuccess(response.data, getState().get('statuses'))); + }).catch(error => { + dispatch(unfollowAccountFail(error)); + }); + }; +}; + +export function followAccountRequest(id) { + return { + type: ACCOUNT_FOLLOW_REQUEST, + id, + }; +}; + +export function followAccountSuccess(relationship, alreadyFollowing) { + return { + type: ACCOUNT_FOLLOW_SUCCESS, + relationship, + alreadyFollowing, + }; +}; + +export function followAccountFail(error) { + return { + type: ACCOUNT_FOLLOW_FAIL, + error, + }; +}; + +export function unfollowAccountRequest(id) { + return { + type: ACCOUNT_UNFOLLOW_REQUEST, + id, + }; +}; + +export function unfollowAccountSuccess(relationship, statuses) { + return { + type: ACCOUNT_UNFOLLOW_SUCCESS, + relationship, + statuses, + }; +}; + +export function unfollowAccountFail(error) { + return { + type: ACCOUNT_UNFOLLOW_FAIL, + error, + }; +}; + +export function blockAccount(id) { + return (dispatch, getState) => { + dispatch(blockAccountRequest(id)); + + api(getState).post(`/api/v1/accounts/${id}/block`).then(response => { + // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers + dispatch(blockAccountSuccess(response.data, getState().get('statuses'))); + }).catch(error => { + dispatch(blockAccountFail(id, error)); + }); + }; +}; + +export function unblockAccount(id) { + return (dispatch, getState) => { + dispatch(unblockAccountRequest(id)); + + api(getState).post(`/api/v1/accounts/${id}/unblock`).then(response => { + dispatch(unblockAccountSuccess(response.data)); + }).catch(error => { + dispatch(unblockAccountFail(id, error)); + }); + }; +}; + +export function blockAccountRequest(id) { + return { + type: ACCOUNT_BLOCK_REQUEST, + id, + }; +}; + +export function blockAccountSuccess(relationship, statuses) { + return { + type: ACCOUNT_BLOCK_SUCCESS, + relationship, + statuses, + }; +}; + +export function blockAccountFail(error) { + return { + type: ACCOUNT_BLOCK_FAIL, + error, + }; +}; + +export function unblockAccountRequest(id) { + return { + type: ACCOUNT_UNBLOCK_REQUEST, + id, + }; +}; + +export function unblockAccountSuccess(relationship) { + return { + type: ACCOUNT_UNBLOCK_SUCCESS, + relationship, + }; +}; + +export function unblockAccountFail(error) { + return { + type: ACCOUNT_UNBLOCK_FAIL, + error, + }; +}; + + +export function muteAccount(id, notifications) { + return (dispatch, getState) => { + dispatch(muteAccountRequest(id)); + + api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications }).then(response => { + // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers + dispatch(muteAccountSuccess(response.data, getState().get('statuses'))); + }).catch(error => { + dispatch(muteAccountFail(id, error)); + }); + }; +}; + +export function unmuteAccount(id) { + return (dispatch, getState) => { + dispatch(unmuteAccountRequest(id)); + + api(getState).post(`/api/v1/accounts/${id}/unmute`).then(response => { + dispatch(unmuteAccountSuccess(response.data)); + }).catch(error => { + dispatch(unmuteAccountFail(id, error)); + }); + }; +}; + +export function muteAccountRequest(id) { + return { + type: ACCOUNT_MUTE_REQUEST, + id, + }; +}; + +export function muteAccountSuccess(relationship, statuses) { + return { + type: ACCOUNT_MUTE_SUCCESS, + relationship, + statuses, + }; +}; + +export function muteAccountFail(error) { + return { + type: ACCOUNT_MUTE_FAIL, + error, + }; +}; + +export function unmuteAccountRequest(id) { + return { + type: ACCOUNT_UNMUTE_REQUEST, + id, + }; +}; + +export function unmuteAccountSuccess(relationship) { + return { + type: ACCOUNT_UNMUTE_SUCCESS, + relationship, + }; +}; + +export function unmuteAccountFail(error) { + return { + type: ACCOUNT_UNMUTE_FAIL, + error, + }; +}; + + +export function fetchFollowers(id) { + return (dispatch, getState) => { + dispatch(fetchFollowersRequest(id)); + + api(getState).get(`/api/v1/accounts/${id}/followers`).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + + dispatch(fetchFollowersSuccess(id, response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map(item => item.id))); + }).catch(error => { + dispatch(fetchFollowersFail(id, error)); + }); + }; +}; + +export function fetchFollowersRequest(id) { + return { + type: FOLLOWERS_FETCH_REQUEST, + id, + }; +}; + +export function fetchFollowersSuccess(id, accounts, next) { + return { + type: FOLLOWERS_FETCH_SUCCESS, + id, + accounts, + next, + }; +}; + +export function fetchFollowersFail(id, error) { + return { + type: FOLLOWERS_FETCH_FAIL, + id, + error, + }; +}; + +export function expandFollowers(id) { + return (dispatch, getState) => { + const url = getState().getIn(['user_lists', 'followers', id, 'next']); + + if (url === null) { + return; + } + + dispatch(expandFollowersRequest(id)); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + + dispatch(expandFollowersSuccess(id, response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map(item => item.id))); + }).catch(error => { + dispatch(expandFollowersFail(id, error)); + }); + }; +}; + +export function expandFollowersRequest(id) { + return { + type: FOLLOWERS_EXPAND_REQUEST, + id, + }; +}; + +export function expandFollowersSuccess(id, accounts, next) { + return { + type: FOLLOWERS_EXPAND_SUCCESS, + id, + accounts, + next, + }; +}; + +export function expandFollowersFail(id, error) { + return { + type: FOLLOWERS_EXPAND_FAIL, + id, + error, + }; +}; + +export function fetchFollowing(id) { + return (dispatch, getState) => { + dispatch(fetchFollowingRequest(id)); + + api(getState).get(`/api/v1/accounts/${id}/following`).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + + dispatch(fetchFollowingSuccess(id, response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map(item => item.id))); + }).catch(error => { + dispatch(fetchFollowingFail(id, error)); + }); + }; +}; + +export function fetchFollowingRequest(id) { + return { + type: FOLLOWING_FETCH_REQUEST, + id, + }; +}; + +export function fetchFollowingSuccess(id, accounts, next) { + return { + type: FOLLOWING_FETCH_SUCCESS, + id, + accounts, + next, + }; +}; + +export function fetchFollowingFail(id, error) { + return { + type: FOLLOWING_FETCH_FAIL, + id, + error, + }; +}; + +export function expandFollowing(id) { + return (dispatch, getState) => { + const url = getState().getIn(['user_lists', 'following', id, 'next']); + + if (url === null) { + return; + } + + dispatch(expandFollowingRequest(id)); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + + dispatch(expandFollowingSuccess(id, response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map(item => item.id))); + }).catch(error => { + dispatch(expandFollowingFail(id, error)); + }); + }; +}; + +export function expandFollowingRequest(id) { + return { + type: FOLLOWING_EXPAND_REQUEST, + id, + }; +}; + +export function expandFollowingSuccess(id, accounts, next) { + return { + type: FOLLOWING_EXPAND_SUCCESS, + id, + accounts, + next, + }; +}; + +export function expandFollowingFail(id, error) { + return { + type: FOLLOWING_EXPAND_FAIL, + id, + error, + }; +}; + +export function fetchRelationships(accountIds) { + return (dispatch, getState) => { + const loadedRelationships = getState().get('relationships'); + const newAccountIds = accountIds.filter(id => loadedRelationships.get(id, null) === null); + + if (newAccountIds.length === 0) { + return; + } + + dispatch(fetchRelationshipsRequest(newAccountIds)); + + api(getState).get(`/api/v1/accounts/relationships?${newAccountIds.map(id => `id[]=${id}`).join('&')}`).then(response => { + dispatch(fetchRelationshipsSuccess(response.data)); + }).catch(error => { + dispatch(fetchRelationshipsFail(error)); + }); + }; +}; + +export function fetchRelationshipsRequest(ids) { + return { + type: RELATIONSHIPS_FETCH_REQUEST, + ids, + skipLoading: true, + }; +}; + +export function fetchRelationshipsSuccess(relationships) { + return { + type: RELATIONSHIPS_FETCH_SUCCESS, + relationships, + skipLoading: true, + }; +}; + +export function fetchRelationshipsFail(error) { + return { + type: RELATIONSHIPS_FETCH_FAIL, + error, + skipLoading: true, + }; +}; + +export function fetchFollowRequests() { + return (dispatch, getState) => { + dispatch(fetchFollowRequestsRequest()); + + api(getState).get('/api/v1/follow_requests').then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(fetchFollowRequestsSuccess(response.data, next ? next.uri : null)); + }).catch(error => dispatch(fetchFollowRequestsFail(error))); + }; +}; + +export function fetchFollowRequestsRequest() { + return { + type: FOLLOW_REQUESTS_FETCH_REQUEST, + }; +}; + +export function fetchFollowRequestsSuccess(accounts, next) { + return { + type: FOLLOW_REQUESTS_FETCH_SUCCESS, + accounts, + next, + }; +}; + +export function fetchFollowRequestsFail(error) { + return { + type: FOLLOW_REQUESTS_FETCH_FAIL, + error, + }; +}; + +export function expandFollowRequests() { + return (dispatch, getState) => { + const url = getState().getIn(['user_lists', 'follow_requests', 'next']); + + if (url === null) { + return; + } + + dispatch(expandFollowRequestsRequest()); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(expandFollowRequestsSuccess(response.data, next ? next.uri : null)); + }).catch(error => dispatch(expandFollowRequestsFail(error))); + }; +}; + +export function expandFollowRequestsRequest() { + return { + type: FOLLOW_REQUESTS_EXPAND_REQUEST, + }; +}; + +export function expandFollowRequestsSuccess(accounts, next) { + return { + type: FOLLOW_REQUESTS_EXPAND_SUCCESS, + accounts, + next, + }; +}; + +export function expandFollowRequestsFail(error) { + return { + type: FOLLOW_REQUESTS_EXPAND_FAIL, + error, + }; +}; + +export function authorizeFollowRequest(id) { + return (dispatch, getState) => { + dispatch(authorizeFollowRequestRequest(id)); + + api(getState) + .post(`/api/v1/follow_requests/${id}/authorize`) + .then(() => dispatch(authorizeFollowRequestSuccess(id))) + .catch(error => dispatch(authorizeFollowRequestFail(id, error))); + }; +}; + +export function authorizeFollowRequestRequest(id) { + return { + type: FOLLOW_REQUEST_AUTHORIZE_REQUEST, + id, + }; +}; + +export function authorizeFollowRequestSuccess(id) { + return { + type: FOLLOW_REQUEST_AUTHORIZE_SUCCESS, + id, + }; +}; + +export function authorizeFollowRequestFail(id, error) { + return { + type: FOLLOW_REQUEST_AUTHORIZE_FAIL, + id, + error, + }; +}; + + +export function rejectFollowRequest(id) { + return (dispatch, getState) => { + dispatch(rejectFollowRequestRequest(id)); + + api(getState) + .post(`/api/v1/follow_requests/${id}/reject`) + .then(() => dispatch(rejectFollowRequestSuccess(id))) + .catch(error => dispatch(rejectFollowRequestFail(id, error))); + }; +}; + +export function rejectFollowRequestRequest(id) { + return { + type: FOLLOW_REQUEST_REJECT_REQUEST, + id, + }; +}; + +export function rejectFollowRequestSuccess(id) { + return { + type: FOLLOW_REQUEST_REJECT_SUCCESS, + id, + }; +}; + +export function rejectFollowRequestFail(id, error) { + return { + type: FOLLOW_REQUEST_REJECT_FAIL, + id, + error, + }; +}; diff --git a/app/javascript/flavours/glitch/actions/alerts.js b/app/javascript/flavours/glitch/actions/alerts.js new file mode 100644 index 000000000..f37fdeeb6 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/alerts.js @@ -0,0 +1,24 @@ +export const ALERT_SHOW = 'ALERT_SHOW'; +export const ALERT_DISMISS = 'ALERT_DISMISS'; +export const ALERT_CLEAR = 'ALERT_CLEAR'; + +export function dismissAlert(alert) { + return { + type: ALERT_DISMISS, + alert, + }; +}; + +export function clearAlert() { + return { + type: ALERT_CLEAR, + }; +}; + +export function showAlert(title, message) { + return { + type: ALERT_SHOW, + title, + message, + }; +}; diff --git a/app/javascript/flavours/glitch/actions/blocks.js b/app/javascript/flavours/glitch/actions/blocks.js new file mode 100644 index 000000000..fe44ca19a --- /dev/null +++ b/app/javascript/flavours/glitch/actions/blocks.js @@ -0,0 +1,82 @@ +import api, { getLinks } from 'flavours/glitch/util/api'; +import { fetchRelationships } from './accounts'; + +export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST'; +export const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS'; +export const BLOCKS_FETCH_FAIL = 'BLOCKS_FETCH_FAIL'; + +export const BLOCKS_EXPAND_REQUEST = 'BLOCKS_EXPAND_REQUEST'; +export const BLOCKS_EXPAND_SUCCESS = 'BLOCKS_EXPAND_SUCCESS'; +export const BLOCKS_EXPAND_FAIL = 'BLOCKS_EXPAND_FAIL'; + +export function fetchBlocks() { + return (dispatch, getState) => { + dispatch(fetchBlocksRequest()); + + api(getState).get('/api/v1/blocks').then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(fetchBlocksSuccess(response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map(item => item.id))); + }).catch(error => dispatch(fetchBlocksFail(error))); + }; +}; + +export function fetchBlocksRequest() { + return { + type: BLOCKS_FETCH_REQUEST, + }; +}; + +export function fetchBlocksSuccess(accounts, next) { + return { + type: BLOCKS_FETCH_SUCCESS, + accounts, + next, + }; +}; + +export function fetchBlocksFail(error) { + return { + type: BLOCKS_FETCH_FAIL, + error, + }; +}; + +export function expandBlocks() { + return (dispatch, getState) => { + const url = getState().getIn(['user_lists', 'blocks', 'next']); + + if (url === null) { + return; + } + + dispatch(expandBlocksRequest()); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(expandBlocksSuccess(response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map(item => item.id))); + }).catch(error => dispatch(expandBlocksFail(error))); + }; +}; + +export function expandBlocksRequest() { + return { + type: BLOCKS_EXPAND_REQUEST, + }; +}; + +export function expandBlocksSuccess(accounts, next) { + return { + type: BLOCKS_EXPAND_SUCCESS, + accounts, + next, + }; +}; + +export function expandBlocksFail(error) { + return { + type: BLOCKS_EXPAND_FAIL, + error, + }; +}; diff --git a/app/javascript/flavours/glitch/actions/bundles.js b/app/javascript/flavours/glitch/actions/bundles.js new file mode 100644 index 000000000..ecc9c8f7d --- /dev/null +++ b/app/javascript/flavours/glitch/actions/bundles.js @@ -0,0 +1,25 @@ +export const BUNDLE_FETCH_REQUEST = 'BUNDLE_FETCH_REQUEST'; +export const BUNDLE_FETCH_SUCCESS = 'BUNDLE_FETCH_SUCCESS'; +export const BUNDLE_FETCH_FAIL = 'BUNDLE_FETCH_FAIL'; + +export function fetchBundleRequest(skipLoading) { + return { + type: BUNDLE_FETCH_REQUEST, + skipLoading, + }; +} + +export function fetchBundleSuccess(skipLoading) { + return { + type: BUNDLE_FETCH_SUCCESS, + skipLoading, + }; +} + +export function fetchBundleFail(error, skipLoading) { + return { + type: BUNDLE_FETCH_FAIL, + error, + skipLoading, + }; +} diff --git a/app/javascript/flavours/glitch/actions/cards.js b/app/javascript/flavours/glitch/actions/cards.js new file mode 100644 index 000000000..c897daf58 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/cards.js @@ -0,0 +1,52 @@ +import api from 'flavours/glitch/util/api'; + +export const STATUS_CARD_FETCH_REQUEST = 'STATUS_CARD_FETCH_REQUEST'; +export const STATUS_CARD_FETCH_SUCCESS = 'STATUS_CARD_FETCH_SUCCESS'; +export const STATUS_CARD_FETCH_FAIL = 'STATUS_CARD_FETCH_FAIL'; + +export function fetchStatusCard(id) { + return (dispatch, getState) => { + if (getState().getIn(['cards', id], null) !== null) { + return; + } + + dispatch(fetchStatusCardRequest(id)); + + api(getState).get(`/api/v1/statuses/${id}/card`).then(response => { + if (!response.data.url) { + return; + } + + dispatch(fetchStatusCardSuccess(id, response.data)); + }).catch(error => { + dispatch(fetchStatusCardFail(id, error)); + }); + }; +}; + +export function fetchStatusCardRequest(id) { + return { + type: STATUS_CARD_FETCH_REQUEST, + id, + skipLoading: true, + }; +}; + +export function fetchStatusCardSuccess(id, card) { + return { + type: STATUS_CARD_FETCH_SUCCESS, + id, + card, + skipLoading: true, + }; +}; + +export function fetchStatusCardFail(id, error) { + return { + type: STATUS_CARD_FETCH_FAIL, + id, + error, + skipLoading: true, + skipAlert: true, + }; +}; diff --git a/app/javascript/flavours/glitch/actions/columns.js b/app/javascript/flavours/glitch/actions/columns.js new file mode 100644 index 000000000..bcb0cdf98 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/columns.js @@ -0,0 +1,40 @@ +import { saveSettings } from './settings'; + +export const COLUMN_ADD = 'COLUMN_ADD'; +export const COLUMN_REMOVE = 'COLUMN_REMOVE'; +export const COLUMN_MOVE = 'COLUMN_MOVE'; + +export function addColumn(id, params) { + return dispatch => { + dispatch({ + type: COLUMN_ADD, + id, + params, + }); + + dispatch(saveSettings()); + }; +}; + +export function removeColumn(uuid) { + return dispatch => { + dispatch({ + type: COLUMN_REMOVE, + uuid, + }); + + dispatch(saveSettings()); + }; +}; + +export function moveColumn(uuid, direction) { + return dispatch => { + dispatch({ + type: COLUMN_MOVE, + uuid, + direction, + }); + + dispatch(saveSettings()); + }; +}; diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js new file mode 100644 index 000000000..32746f27b --- /dev/null +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -0,0 +1,398 @@ +import api from 'flavours/glitch/util/api'; +import { throttle } from 'lodash'; +import { search as emojiSearch } from 'flavours/glitch/util/emoji/emoji_mart_search_light'; +import { useEmoji } from './emojis'; + +import { + updateTimeline, + refreshHomeTimeline, + refreshCommunityTimeline, + refreshPublicTimeline, + refreshDirectTimeline, +} from './timelines'; + +export const COMPOSE_CHANGE = 'COMPOSE_CHANGE'; +export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST'; +export const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS'; +export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL'; +export const COMPOSE_REPLY = 'COMPOSE_REPLY'; +export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL'; +export const COMPOSE_MENTION = 'COMPOSE_MENTION'; +export const COMPOSE_RESET = 'COMPOSE_RESET'; +export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST'; +export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS'; +export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL'; +export const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS'; +export const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO'; + +export const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR'; +export const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY'; +export const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT'; + +export const COMPOSE_MOUNT = 'COMPOSE_MOUNT'; +export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT'; + +export const COMPOSE_ADVANCED_OPTIONS_CHANGE = 'COMPOSE_ADVANCED_OPTIONS_CHANGE'; +export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE'; +export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE'; +export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE'; +export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE'; +export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE'; +export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE'; + +export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT'; + +export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST'; +export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS'; +export const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL'; + +export const COMPOSE_DOODLE_SET = 'COMPOSE_DOODLE_SET'; + +export function changeCompose(text) { + return { + type: COMPOSE_CHANGE, + text: text, + }; +}; + +export function replyCompose(status, router) { + return (dispatch, getState) => { + dispatch({ + type: COMPOSE_REPLY, + status: status, + }); + + if (!getState().getIn(['compose', 'mounted'])) { + router.push('/statuses/new'); + } + }; +}; + +export function cancelReplyCompose() { + return { + type: COMPOSE_REPLY_CANCEL, + }; +}; + +export function resetCompose() { + return { + type: COMPOSE_RESET, + }; +}; + +export function mentionCompose(account, router) { + return (dispatch, getState) => { + dispatch({ + type: COMPOSE_MENTION, + account: account, + }); + + if (!getState().getIn(['compose', 'mounted'])) { + router.push('/statuses/new'); + } + }; +}; + +export function submitCompose() { + return function (dispatch, getState) { + let status = getState().getIn(['compose', 'text'], ''); + + if (!status || !status.length) { + return; + } + + dispatch(submitComposeRequest()); + if (getState().getIn(['compose', 'advanced_options', 'do_not_federate'])) { + status = status + ' 👁️'; + } + api(getState).post('/api/v1/statuses', { + status, + in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), + media_ids: getState().getIn(['compose', 'media_attachments']).map(item => item.get('id')), + sensitive: getState().getIn(['compose', 'sensitive']), + spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''), + visibility: getState().getIn(['compose', 'privacy']), + }, { + headers: { + 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), + }, + }).then(function (response) { + dispatch(submitComposeSuccess({ ...response.data })); + + // To make the app more responsive, immediately get the status into the columns + + const insertOrRefresh = (timelineId, refreshAction) => { + if (getState().getIn(['timelines', timelineId, 'online'])) { + dispatch(updateTimeline(timelineId, { ...response.data })); + } else if (getState().getIn(['timelines', timelineId, 'loaded'])) { + dispatch(refreshAction()); + } + }; + + insertOrRefresh('home', refreshHomeTimeline); + + if (response.data.in_reply_to_id === null && response.data.visibility === 'public') { + insertOrRefresh('community', refreshCommunityTimeline); + insertOrRefresh('public', refreshPublicTimeline); + } else if (response.data.visibility === 'direct') { + insertOrRefresh('direct', refreshDirectTimeline); + } + }).catch(function (error) { + dispatch(submitComposeFail(error)); + }); + }; +}; + +export function submitComposeRequest() { + return { + type: COMPOSE_SUBMIT_REQUEST, + }; +}; + +export function submitComposeSuccess(status) { + return { + type: COMPOSE_SUBMIT_SUCCESS, + status: status, + }; +}; + +export function submitComposeFail(error) { + return { + type: COMPOSE_SUBMIT_FAIL, + error: error, + }; +}; + +export function doodleSet(options) { + return { + type: COMPOSE_DOODLE_SET, + options: options, + }; +}; + +export function uploadCompose(files) { + return function (dispatch, getState) { + if (getState().getIn(['compose', 'media_attachments']).size > 3) { + return; + } + + dispatch(uploadComposeRequest()); + + let data = new FormData(); + data.append('file', files[0]); + + api(getState).post('/api/v1/media', data, { + onUploadProgress: function (e) { + dispatch(uploadComposeProgress(e.loaded, e.total)); + }, + }).then(function (response) { + dispatch(uploadComposeSuccess(response.data)); + }).catch(function (error) { + dispatch(uploadComposeFail(error)); + }); + }; +}; + +export function changeUploadCompose(id, description) { + return (dispatch, getState) => { + dispatch(changeUploadComposeRequest()); + + api(getState).put(`/api/v1/media/${id}`, { description }).then(response => { + dispatch(changeUploadComposeSuccess(response.data)); + }).catch(error => { + dispatch(changeUploadComposeFail(id, error)); + }); + }; +}; + +export function changeUploadComposeRequest() { + return { + type: COMPOSE_UPLOAD_CHANGE_REQUEST, + skipLoading: true, + }; +}; +export function changeUploadComposeSuccess(media) { + return { + type: COMPOSE_UPLOAD_CHANGE_SUCCESS, + media: media, + skipLoading: true, + }; +}; + +export function changeUploadComposeFail(error) { + return { + type: COMPOSE_UPLOAD_CHANGE_FAIL, + error: error, + skipLoading: true, + }; +}; + +export function uploadComposeRequest() { + return { + type: COMPOSE_UPLOAD_REQUEST, + skipLoading: true, + }; +}; + +export function uploadComposeProgress(loaded, total) { + return { + type: COMPOSE_UPLOAD_PROGRESS, + loaded: loaded, + total: total, + }; +}; + +export function uploadComposeSuccess(media) { + return { + type: COMPOSE_UPLOAD_SUCCESS, + media: media, + skipLoading: true, + }; +}; + +export function uploadComposeFail(error) { + return { + type: COMPOSE_UPLOAD_FAIL, + error: error, + skipLoading: true, + }; +}; + +export function undoUploadCompose(media_id) { + return { + type: COMPOSE_UPLOAD_UNDO, + media_id: media_id, + }; +}; + +export function clearComposeSuggestions() { + return { + type: COMPOSE_SUGGESTIONS_CLEAR, + }; +}; + +const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => { + api(getState).get('/api/v1/accounts/search', { + params: { + q: token.slice(1), + resolve: false, + limit: 4, + }, + }).then(response => { + dispatch(readyComposeSuggestionsAccounts(token, response.data)); + }); +}, 200, { leading: true, trailing: true }); + +const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => { + const results = emojiSearch(token.replace(':', ''), { maxResults: 5 }); + dispatch(readyComposeSuggestionsEmojis(token, results)); +}; + +export function fetchComposeSuggestions(token) { + return (dispatch, getState) => { + if (token[0] === ':') { + fetchComposeSuggestionsEmojis(dispatch, getState, token); + } else { + fetchComposeSuggestionsAccounts(dispatch, getState, token); + } + }; +}; + +export function readyComposeSuggestionsEmojis(token, emojis) { + return { + type: COMPOSE_SUGGESTIONS_READY, + token, + emojis, + }; +}; + +export function readyComposeSuggestionsAccounts(token, accounts) { + return { + type: COMPOSE_SUGGESTIONS_READY, + token, + accounts, + }; +}; + +export function selectComposeSuggestion(position, token, suggestion) { + return (dispatch, getState) => { + let completion, startPosition; + + if (typeof suggestion === 'object' && suggestion.id) { + completion = suggestion.native || suggestion.colons; + startPosition = position - 1; + + dispatch(useEmoji(suggestion)); + } else { + completion = getState().getIn(['accounts', suggestion, 'acct']); + startPosition = position; + } + + dispatch({ + type: COMPOSE_SUGGESTION_SELECT, + position: startPosition, + token, + completion, + }); + }; +}; + +export function mountCompose() { + return { + type: COMPOSE_MOUNT, + }; +}; + +export function unmountCompose() { + return { + type: COMPOSE_UNMOUNT, + }; +}; + +export function toggleComposeAdvancedOption(option) { + return { + type: COMPOSE_ADVANCED_OPTIONS_CHANGE, + option: option, + }; +} + +export function changeComposeSensitivity() { + return { + type: COMPOSE_SENSITIVITY_CHANGE, + }; +}; + +export function changeComposeSpoilerness() { + return { + type: COMPOSE_SPOILERNESS_CHANGE, + }; +}; + +export function changeComposeSpoilerText(text) { + return { + type: COMPOSE_SPOILER_TEXT_CHANGE, + text, + }; +}; + +export function changeComposeVisibility(value) { + return { + type: COMPOSE_VISIBILITY_CHANGE, + value, + }; +}; + +export function insertEmojiCompose(position, emoji) { + return { + type: COMPOSE_EMOJI_INSERT, + position, + emoji, + }; +}; + +export function changeComposing(value) { + return { + type: COMPOSE_COMPOSING_CHANGE, + value, + }; +} diff --git a/app/javascript/flavours/glitch/actions/domain_blocks.js b/app/javascript/flavours/glitch/actions/domain_blocks.js new file mode 100644 index 000000000..8506df91c --- /dev/null +++ b/app/javascript/flavours/glitch/actions/domain_blocks.js @@ -0,0 +1,117 @@ +import api, { getLinks } from 'flavours/glitch/util/api'; + +export const DOMAIN_BLOCK_REQUEST = 'DOMAIN_BLOCK_REQUEST'; +export const DOMAIN_BLOCK_SUCCESS = 'DOMAIN_BLOCK_SUCCESS'; +export const DOMAIN_BLOCK_FAIL = 'DOMAIN_BLOCK_FAIL'; + +export const DOMAIN_UNBLOCK_REQUEST = 'DOMAIN_UNBLOCK_REQUEST'; +export const DOMAIN_UNBLOCK_SUCCESS = 'DOMAIN_UNBLOCK_SUCCESS'; +export const DOMAIN_UNBLOCK_FAIL = 'DOMAIN_UNBLOCK_FAIL'; + +export const DOMAIN_BLOCKS_FETCH_REQUEST = 'DOMAIN_BLOCKS_FETCH_REQUEST'; +export const DOMAIN_BLOCKS_FETCH_SUCCESS = 'DOMAIN_BLOCKS_FETCH_SUCCESS'; +export const DOMAIN_BLOCKS_FETCH_FAIL = 'DOMAIN_BLOCKS_FETCH_FAIL'; + +export function blockDomain(domain, accountId) { + return (dispatch, getState) => { + dispatch(blockDomainRequest(domain)); + + api(getState).post('/api/v1/domain_blocks', { domain }).then(() => { + dispatch(blockDomainSuccess(domain, accountId)); + }).catch(err => { + dispatch(blockDomainFail(domain, err)); + }); + }; +}; + +export function blockDomainRequest(domain) { + return { + type: DOMAIN_BLOCK_REQUEST, + domain, + }; +}; + +export function blockDomainSuccess(domain, accountId) { + return { + type: DOMAIN_BLOCK_SUCCESS, + domain, + accountId, + }; +}; + +export function blockDomainFail(domain, error) { + return { + type: DOMAIN_BLOCK_FAIL, + domain, + error, + }; +}; + +export function unblockDomain(domain, accountId) { + return (dispatch, getState) => { + dispatch(unblockDomainRequest(domain)); + + api(getState).delete('/api/v1/domain_blocks', { params: { domain } }).then(() => { + dispatch(unblockDomainSuccess(domain, accountId)); + }).catch(err => { + dispatch(unblockDomainFail(domain, err)); + }); + }; +}; + +export function unblockDomainRequest(domain) { + return { + type: DOMAIN_UNBLOCK_REQUEST, + domain, + }; +}; + +export function unblockDomainSuccess(domain, accountId) { + return { + type: DOMAIN_UNBLOCK_SUCCESS, + domain, + accountId, + }; +}; + +export function unblockDomainFail(domain, error) { + return { + type: DOMAIN_UNBLOCK_FAIL, + domain, + error, + }; +}; + +export function fetchDomainBlocks() { + return (dispatch, getState) => { + dispatch(fetchDomainBlocksRequest()); + + api(getState).get().then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(fetchDomainBlocksSuccess(response.data, next ? next.uri : null)); + }).catch(err => { + dispatch(fetchDomainBlocksFail(err)); + }); + }; +}; + +export function fetchDomainBlocksRequest() { + return { + type: DOMAIN_BLOCKS_FETCH_REQUEST, + }; +}; + +export function fetchDomainBlocksSuccess(domains, next) { + return { + type: DOMAIN_BLOCKS_FETCH_SUCCESS, + domains, + next, + }; +}; + +export function fetchDomainBlocksFail(error) { + return { + type: DOMAIN_BLOCKS_FETCH_FAIL, + error, + }; +}; diff --git a/app/javascript/flavours/glitch/actions/emojis.js b/app/javascript/flavours/glitch/actions/emojis.js new file mode 100644 index 000000000..7cd9d4b7b --- /dev/null +++ b/app/javascript/flavours/glitch/actions/emojis.js @@ -0,0 +1,14 @@ +import { saveSettings } from './settings'; + +export const EMOJI_USE = 'EMOJI_USE'; + +export function useEmoji(emoji) { + return dispatch => { + dispatch({ + type: EMOJI_USE, + emoji, + }); + + dispatch(saveSettings()); + }; +}; diff --git a/app/javascript/flavours/glitch/actions/favourites.js b/app/javascript/flavours/glitch/actions/favourites.js new file mode 100644 index 000000000..decdcee4f --- /dev/null +++ b/app/javascript/flavours/glitch/actions/favourites.js @@ -0,0 +1,83 @@ +import api, { getLinks } from 'flavours/glitch/util/api'; + +export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST'; +export const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS'; +export const FAVOURITED_STATUSES_FETCH_FAIL = 'FAVOURITED_STATUSES_FETCH_FAIL'; + +export const FAVOURITED_STATUSES_EXPAND_REQUEST = 'FAVOURITED_STATUSES_EXPAND_REQUEST'; +export const FAVOURITED_STATUSES_EXPAND_SUCCESS = 'FAVOURITED_STATUSES_EXPAND_SUCCESS'; +export const FAVOURITED_STATUSES_EXPAND_FAIL = 'FAVOURITED_STATUSES_EXPAND_FAIL'; + +export function fetchFavouritedStatuses() { + return (dispatch, getState) => { + dispatch(fetchFavouritedStatusesRequest()); + + api(getState).get('/api/v1/favourites').then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null)); + }).catch(error => { + dispatch(fetchFavouritedStatusesFail(error)); + }); + }; +}; + +export function fetchFavouritedStatusesRequest() { + return { + type: FAVOURITED_STATUSES_FETCH_REQUEST, + }; +}; + +export function fetchFavouritedStatusesSuccess(statuses, next) { + return { + type: FAVOURITED_STATUSES_FETCH_SUCCESS, + statuses, + next, + }; +}; + +export function fetchFavouritedStatusesFail(error) { + return { + type: FAVOURITED_STATUSES_FETCH_FAIL, + error, + }; +}; + +export function expandFavouritedStatuses() { + return (dispatch, getState) => { + const url = getState().getIn(['status_lists', 'favourites', 'next'], null); + + if (url === null) { + return; + } + + dispatch(expandFavouritedStatusesRequest()); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null)); + }).catch(error => { + dispatch(expandFavouritedStatusesFail(error)); + }); + }; +}; + +export function expandFavouritedStatusesRequest() { + return { + type: FAVOURITED_STATUSES_EXPAND_REQUEST, + }; +}; + +export function expandFavouritedStatusesSuccess(statuses, next) { + return { + type: FAVOURITED_STATUSES_EXPAND_SUCCESS, + statuses, + next, + }; +}; + +export function expandFavouritedStatusesFail(error) { + return { + type: FAVOURITED_STATUSES_EXPAND_FAIL, + error, + }; +}; diff --git a/app/javascript/flavours/glitch/actions/height_cache.js b/app/javascript/flavours/glitch/actions/height_cache.js new file mode 100644 index 000000000..4c752993f --- /dev/null +++ b/app/javascript/flavours/glitch/actions/height_cache.js @@ -0,0 +1,17 @@ +export const HEIGHT_CACHE_SET = 'HEIGHT_CACHE_SET'; +export const HEIGHT_CACHE_CLEAR = 'HEIGHT_CACHE_CLEAR'; + +export function setHeight (key, id, height) { + return { + type: HEIGHT_CACHE_SET, + key, + id, + height, + }; +}; + +export function clearHeight () { + return { + type: HEIGHT_CACHE_CLEAR, + }; +}; diff --git a/app/javascript/flavours/glitch/actions/interactions.js b/app/javascript/flavours/glitch/actions/interactions.js new file mode 100644 index 000000000..ceeb2773b --- /dev/null +++ b/app/javascript/flavours/glitch/actions/interactions.js @@ -0,0 +1,313 @@ +import api from 'flavours/glitch/util/api'; + +export const REBLOG_REQUEST = 'REBLOG_REQUEST'; +export const REBLOG_SUCCESS = 'REBLOG_SUCCESS'; +export const REBLOG_FAIL = 'REBLOG_FAIL'; + +export const FAVOURITE_REQUEST = 'FAVOURITE_REQUEST'; +export const FAVOURITE_SUCCESS = 'FAVOURITE_SUCCESS'; +export const FAVOURITE_FAIL = 'FAVOURITE_FAIL'; + +export const UNREBLOG_REQUEST = 'UNREBLOG_REQUEST'; +export const UNREBLOG_SUCCESS = 'UNREBLOG_SUCCESS'; +export const UNREBLOG_FAIL = 'UNREBLOG_FAIL'; + +export const UNFAVOURITE_REQUEST = 'UNFAVOURITE_REQUEST'; +export const UNFAVOURITE_SUCCESS = 'UNFAVOURITE_SUCCESS'; +export const UNFAVOURITE_FAIL = 'UNFAVOURITE_FAIL'; + +export const REBLOGS_FETCH_REQUEST = 'REBLOGS_FETCH_REQUEST'; +export const REBLOGS_FETCH_SUCCESS = 'REBLOGS_FETCH_SUCCESS'; +export const REBLOGS_FETCH_FAIL = 'REBLOGS_FETCH_FAIL'; + +export const FAVOURITES_FETCH_REQUEST = 'FAVOURITES_FETCH_REQUEST'; +export const FAVOURITES_FETCH_SUCCESS = 'FAVOURITES_FETCH_SUCCESS'; +export const FAVOURITES_FETCH_FAIL = 'FAVOURITES_FETCH_FAIL'; + +export const PIN_REQUEST = 'PIN_REQUEST'; +export const PIN_SUCCESS = 'PIN_SUCCESS'; +export const PIN_FAIL = 'PIN_FAIL'; + +export const UNPIN_REQUEST = 'UNPIN_REQUEST'; +export const UNPIN_SUCCESS = 'UNPIN_SUCCESS'; +export const UNPIN_FAIL = 'UNPIN_FAIL'; + +export function reblog(status) { + return function (dispatch, getState) { + dispatch(reblogRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`).then(function (response) { + // The reblog API method returns a new status wrapped around the original. In this case we are only + // interested in how the original is modified, hence passing it skipping the wrapper + dispatch(reblogSuccess(status, response.data.reblog)); + }).catch(function (error) { + dispatch(reblogFail(status, error)); + }); + }; +}; + +export function unreblog(status) { + return (dispatch, getState) => { + dispatch(unreblogRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.get('id')}/unreblog`).then(response => { + dispatch(unreblogSuccess(status, response.data)); + }).catch(error => { + dispatch(unreblogFail(status, error)); + }); + }; +}; + +export function reblogRequest(status) { + return { + type: REBLOG_REQUEST, + status: status, + }; +}; + +export function reblogSuccess(status, response) { + return { + type: REBLOG_SUCCESS, + status: status, + response: response, + }; +}; + +export function reblogFail(status, error) { + return { + type: REBLOG_FAIL, + status: status, + error: error, + }; +}; + +export function unreblogRequest(status) { + return { + type: UNREBLOG_REQUEST, + status: status, + }; +}; + +export function unreblogSuccess(status, response) { + return { + type: UNREBLOG_SUCCESS, + status: status, + response: response, + }; +}; + +export function unreblogFail(status, error) { + return { + type: UNREBLOG_FAIL, + status: status, + error: error, + }; +}; + +export function favourite(status) { + return function (dispatch, getState) { + dispatch(favouriteRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.get('id')}/favourite`).then(function (response) { + dispatch(favouriteSuccess(status, response.data)); + }).catch(function (error) { + dispatch(favouriteFail(status, error)); + }); + }; +}; + +export function unfavourite(status) { + return (dispatch, getState) => { + dispatch(unfavouriteRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.get('id')}/unfavourite`).then(response => { + dispatch(unfavouriteSuccess(status, response.data)); + }).catch(error => { + dispatch(unfavouriteFail(status, error)); + }); + }; +}; + +export function favouriteRequest(status) { + return { + type: FAVOURITE_REQUEST, + status: status, + }; +}; + +export function favouriteSuccess(status, response) { + return { + type: FAVOURITE_SUCCESS, + status: status, + response: response, + }; +}; + +export function favouriteFail(status, error) { + return { + type: FAVOURITE_FAIL, + status: status, + error: error, + }; +}; + +export function unfavouriteRequest(status) { + return { + type: UNFAVOURITE_REQUEST, + status: status, + }; +}; + +export function unfavouriteSuccess(status, response) { + return { + type: UNFAVOURITE_SUCCESS, + status: status, + response: response, + }; +}; + +export function unfavouriteFail(status, error) { + return { + type: UNFAVOURITE_FAIL, + status: status, + error: error, + }; +}; + +export function fetchReblogs(id) { + return (dispatch, getState) => { + dispatch(fetchReblogsRequest(id)); + + api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => { + dispatch(fetchReblogsSuccess(id, response.data)); + }).catch(error => { + dispatch(fetchReblogsFail(id, error)); + }); + }; +}; + +export function fetchReblogsRequest(id) { + return { + type: REBLOGS_FETCH_REQUEST, + id, + }; +}; + +export function fetchReblogsSuccess(id, accounts) { + return { + type: REBLOGS_FETCH_SUCCESS, + id, + accounts, + }; +}; + +export function fetchReblogsFail(id, error) { + return { + type: REBLOGS_FETCH_FAIL, + error, + }; +}; + +export function fetchFavourites(id) { + return (dispatch, getState) => { + dispatch(fetchFavouritesRequest(id)); + + api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => { + dispatch(fetchFavouritesSuccess(id, response.data)); + }).catch(error => { + dispatch(fetchFavouritesFail(id, error)); + }); + }; +}; + +export function fetchFavouritesRequest(id) { + return { + type: FAVOURITES_FETCH_REQUEST, + id, + }; +}; + +export function fetchFavouritesSuccess(id, accounts) { + return { + type: FAVOURITES_FETCH_SUCCESS, + id, + accounts, + }; +}; + +export function fetchFavouritesFail(id, error) { + return { + type: FAVOURITES_FETCH_FAIL, + error, + }; +}; + +export function pin(status) { + return (dispatch, getState) => { + dispatch(pinRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => { + dispatch(pinSuccess(status, response.data)); + }).catch(error => { + dispatch(pinFail(status, error)); + }); + }; +}; + +export function pinRequest(status) { + return { + type: PIN_REQUEST, + status, + }; +}; + +export function pinSuccess(status, response) { + return { + type: PIN_SUCCESS, + status, + response, + }; +}; + +export function pinFail(status, error) { + return { + type: PIN_FAIL, + status, + error, + }; +}; + +export function unpin (status) { + return (dispatch, getState) => { + dispatch(unpinRequest(status)); + + api(getState).post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => { + dispatch(unpinSuccess(status, response.data)); + }).catch(error => { + dispatch(unpinFail(status, error)); + }); + }; +}; + +export function unpinRequest(status) { + return { + type: UNPIN_REQUEST, + status, + }; +}; + +export function unpinSuccess(status, response) { + return { + type: UNPIN_SUCCESS, + status, + response, + }; +}; + +export function unpinFail(status, error) { + return { + type: UNPIN_FAIL, + status, + error, + }; +}; diff --git a/app/javascript/flavours/glitch/actions/local_settings.js b/app/javascript/flavours/glitch/actions/local_settings.js new file mode 100644 index 000000000..28660a4e8 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/local_settings.js @@ -0,0 +1,24 @@ +export const LOCAL_SETTING_CHANGE = 'LOCAL_SETTING_CHANGE'; + +export function changeLocalSetting(key, value) { + return dispatch => { + dispatch({ + type: LOCAL_SETTING_CHANGE, + key, + value, + }); + + dispatch(saveLocalSettings()); + }; +}; + +// __TODO :__ +// Right now `saveLocalSettings()` doesn't keep track of which user +// is currently signed in, but it might be better to give each user +// their *own* local settings. +export function saveLocalSettings() { + return (_, getState) => { + const localSettings = getState().get('local_settings').toJS(); + localStorage.setItem('mastodon-settings', JSON.stringify(localSettings)); + }; +}; diff --git a/app/javascript/flavours/glitch/actions/modal.js b/app/javascript/flavours/glitch/actions/modal.js new file mode 100644 index 000000000..80e15c28e --- /dev/null +++ b/app/javascript/flavours/glitch/actions/modal.js @@ -0,0 +1,16 @@ +export const MODAL_OPEN = 'MODAL_OPEN'; +export const MODAL_CLOSE = 'MODAL_CLOSE'; + +export function openModal(type, props) { + return { + type: MODAL_OPEN, + modalType: type, + modalProps: props, + }; +}; + +export function closeModal() { + return { + type: MODAL_CLOSE, + }; +}; diff --git a/app/javascript/flavours/glitch/actions/mutes.js b/app/javascript/flavours/glitch/actions/mutes.js new file mode 100644 index 000000000..e06130533 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/mutes.js @@ -0,0 +1,103 @@ +import api, { getLinks } from 'flavours/glitch/util/api'; +import { fetchRelationships } from './accounts'; +import { openModal } from 'flavours/glitch/actions/modal'; + +export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST'; +export const MUTES_FETCH_SUCCESS = 'MUTES_FETCH_SUCCESS'; +export const MUTES_FETCH_FAIL = 'MUTES_FETCH_FAIL'; + +export const MUTES_EXPAND_REQUEST = 'MUTES_EXPAND_REQUEST'; +export const MUTES_EXPAND_SUCCESS = 'MUTES_EXPAND_SUCCESS'; +export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL'; + +export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL'; +export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS'; + +export function fetchMutes() { + return (dispatch, getState) => { + dispatch(fetchMutesRequest()); + + api(getState).get('/api/v1/mutes').then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(fetchMutesSuccess(response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map(item => item.id))); + }).catch(error => dispatch(fetchMutesFail(error))); + }; +}; + +export function fetchMutesRequest() { + return { + type: MUTES_FETCH_REQUEST, + }; +}; + +export function fetchMutesSuccess(accounts, next) { + return { + type: MUTES_FETCH_SUCCESS, + accounts, + next, + }; +}; + +export function fetchMutesFail(error) { + return { + type: MUTES_FETCH_FAIL, + error, + }; +}; + +export function expandMutes() { + return (dispatch, getState) => { + const url = getState().getIn(['user_lists', 'mutes', 'next']); + + if (url === null) { + return; + } + + dispatch(expandMutesRequest()); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(expandMutesSuccess(response.data, next ? next.uri : null)); + dispatch(fetchRelationships(response.data.map(item => item.id))); + }).catch(error => dispatch(expandMutesFail(error))); + }; +}; + +export function expandMutesRequest() { + return { + type: MUTES_EXPAND_REQUEST, + }; +}; + +export function expandMutesSuccess(accounts, next) { + return { + type: MUTES_EXPAND_SUCCESS, + accounts, + next, + }; +}; + +export function expandMutesFail(error) { + return { + type: MUTES_EXPAND_FAIL, + error, + }; +}; + +export function initMuteModal(account) { + return dispatch => { + dispatch({ + type: MUTES_INIT_MODAL, + account, + }); + + dispatch(openModal('MUTE')); + }; +} + +export function toggleHideNotifications() { + return dispatch => { + dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS }); + }; +} diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js new file mode 100644 index 000000000..9b9ebf86d --- /dev/null +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -0,0 +1,265 @@ +import api, { getLinks } from 'flavours/glitch/util/api'; +import { List as ImmutableList } from 'immutable'; +import IntlMessageFormat from 'intl-messageformat'; +import { fetchRelationships } from './accounts'; +import { defineMessages } from 'react-intl'; + +export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; + +// tracking the notif cleaning request +export const NOTIFICATIONS_DELETE_MARKED_REQUEST = 'NOTIFICATIONS_DELETE_MARKED_REQUEST'; +export const NOTIFICATIONS_DELETE_MARKED_SUCCESS = 'NOTIFICATIONS_DELETE_MARKED_SUCCESS'; +export const NOTIFICATIONS_DELETE_MARKED_FAIL = 'NOTIFICATIONS_DELETE_MARKED_FAIL'; +export const NOTIFICATIONS_MARK_ALL_FOR_DELETE = 'NOTIFICATIONS_MARK_ALL_FOR_DELETE'; +export const NOTIFICATIONS_ENTER_CLEARING_MODE = 'NOTIFICATIONS_ENTER_CLEARING_MODE'; // arg: yes +// Unmark notifications (when the cleaning mode is left) +export const NOTIFICATIONS_UNMARK_ALL_FOR_DELETE = 'NOTIFICATIONS_UNMARK_ALL_FOR_DELETE'; +// Mark one for delete +export const NOTIFICATION_MARK_FOR_DELETE = 'NOTIFICATION_MARK_FOR_DELETE'; + +export const NOTIFICATIONS_REFRESH_REQUEST = 'NOTIFICATIONS_REFRESH_REQUEST'; +export const NOTIFICATIONS_REFRESH_SUCCESS = 'NOTIFICATIONS_REFRESH_SUCCESS'; +export const NOTIFICATIONS_REFRESH_FAIL = 'NOTIFICATIONS_REFRESH_FAIL'; + +export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST'; +export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS'; +export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL'; + +export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR'; +export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; + +defineMessages({ + mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, +}); + +const fetchRelatedRelationships = (dispatch, notifications) => { + const accountIds = notifications.filter(item => item.type === 'follow').map(item => item.account.id); + + if (accountIds > 0) { + dispatch(fetchRelationships(accountIds)); + } +}; + +const unescapeHTML = (html) => { + const wrapper = document.createElement('div'); + html = html.replace(/
|
|\n/, ' '); + wrapper.innerHTML = html; + return wrapper.textContent; +}; + +export function updateNotifications(notification, intlMessages, intlLocale) { + return (dispatch, getState) => { + const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true); + const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true); + + dispatch({ + type: NOTIFICATIONS_UPDATE, + notification, + account: notification.account, + status: notification.status, + meta: playSound ? { sound: 'boop' } : undefined, + }); + + fetchRelatedRelationships(dispatch, [notification]); + + // Desktop notifications + if (typeof window.Notification !== 'undefined' && showAlert) { + const title = new IntlMessageFormat(intlMessages[`notification.${notification.type}`], intlLocale).format({ name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username }); + const body = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : ''); + + const notify = new Notification(title, { body, icon: notification.account.avatar, tag: notification.id }); + notify.addEventListener('click', () => { + window.focus(); + notify.close(); + }); + } + }; +}; + +const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS(); + +export function refreshNotifications() { + return (dispatch, getState) => { + const params = {}; + const ids = getState().getIn(['notifications', 'items']); + + let skipLoading = false; + + if (ids.size > 0) { + params.since_id = ids.first().get('id'); + } + + if (getState().getIn(['notifications', 'loaded'])) { + skipLoading = true; + } + + params.exclude_types = excludeTypesFromSettings(getState()); + + dispatch(refreshNotificationsRequest(skipLoading)); + + api(getState).get('/api/v1/notifications', { params }).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + + dispatch(refreshNotificationsSuccess(response.data, skipLoading, next ? next.uri : null)); + fetchRelatedRelationships(dispatch, response.data); + }).catch(error => { + dispatch(refreshNotificationsFail(error, skipLoading)); + }); + }; +}; + +export function refreshNotificationsRequest(skipLoading) { + return { + type: NOTIFICATIONS_REFRESH_REQUEST, + skipLoading, + }; +}; + +export function refreshNotificationsSuccess(notifications, skipLoading, next) { + return { + type: NOTIFICATIONS_REFRESH_SUCCESS, + notifications, + accounts: notifications.map(item => item.account), + statuses: notifications.map(item => item.status).filter(status => !!status), + skipLoading, + next, + }; +}; + +export function refreshNotificationsFail(error, skipLoading) { + return { + type: NOTIFICATIONS_REFRESH_FAIL, + error, + skipLoading, + }; +}; + +export function expandNotifications() { + return (dispatch, getState) => { + const items = getState().getIn(['notifications', 'items'], ImmutableList()); + + if (getState().getIn(['notifications', 'isLoading']) || items.size === 0) { + return; + } + + const params = { + max_id: items.last().get('id'), + limit: 20, + exclude_types: excludeTypesFromSettings(getState()), + }; + + dispatch(expandNotificationsRequest()); + + api(getState).get('/api/v1/notifications', { params }).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null)); + fetchRelatedRelationships(dispatch, response.data); + }).catch(error => { + dispatch(expandNotificationsFail(error)); + }); + }; +}; + +export function expandNotificationsRequest() { + return { + type: NOTIFICATIONS_EXPAND_REQUEST, + }; +}; + +export function expandNotificationsSuccess(notifications, next) { + return { + type: NOTIFICATIONS_EXPAND_SUCCESS, + notifications, + accounts: notifications.map(item => item.account), + statuses: notifications.map(item => item.status).filter(status => !!status), + next, + }; +}; + +export function expandNotificationsFail(error) { + return { + type: NOTIFICATIONS_EXPAND_FAIL, + error, + }; +}; + +export function clearNotifications() { + return (dispatch, getState) => { + dispatch({ + type: NOTIFICATIONS_CLEAR, + }); + + api(getState).post('/api/v1/notifications/clear'); + }; +}; + +export function scrollTopNotifications(top) { + return { + type: NOTIFICATIONS_SCROLL_TOP, + top, + }; +}; + +export function deleteMarkedNotifications() { + return (dispatch, getState) => { + dispatch(deleteMarkedNotificationsRequest()); + + let ids = []; + getState().getIn(['notifications', 'items']).forEach((n) => { + if (n.get('markedForDelete')) { + ids.push(n.get('id')); + } + }); + + if (ids.length === 0) { + return; + } + + api(getState).delete(`/api/v1/notifications/destroy_multiple?ids[]=${ids.join('&ids[]=')}`).then(() => { + dispatch(deleteMarkedNotificationsSuccess()); + }).catch(error => { + console.error(error); + dispatch(deleteMarkedNotificationsFail(error)); + }); + }; +}; + +export function enterNotificationClearingMode(yes) { + return { + type: NOTIFICATIONS_ENTER_CLEARING_MODE, + yes: yes, + }; +}; + +export function markAllNotifications(yes) { + return { + type: NOTIFICATIONS_MARK_ALL_FOR_DELETE, + yes: yes, // true, false or null. null = invert + }; +}; + +export function deleteMarkedNotificationsRequest() { + return { + type: NOTIFICATIONS_DELETE_MARKED_REQUEST, + }; +}; + +export function deleteMarkedNotificationsFail() { + return { + type: NOTIFICATIONS_DELETE_MARKED_FAIL, + }; +}; + +export function markNotificationForDelete(id, yes) { + return { + type: NOTIFICATION_MARK_FOR_DELETE, + id: id, + yes: yes, + }; +}; + +export function deleteMarkedNotificationsSuccess() { + return { + type: NOTIFICATIONS_DELETE_MARKED_SUCCESS, + }; +}; diff --git a/app/javascript/flavours/glitch/actions/onboarding.js b/app/javascript/flavours/glitch/actions/onboarding.js new file mode 100644 index 000000000..a161c50ef --- /dev/null +++ b/app/javascript/flavours/glitch/actions/onboarding.js @@ -0,0 +1,14 @@ +import { openModal } from './modal'; +import { changeSetting, saveSettings } from './settings'; + +export function showOnboardingOnce() { + return (dispatch, getState) => { + const alreadySeen = getState().getIn(['settings', 'onboarded']); + + if (!alreadySeen) { + dispatch(openModal('ONBOARDING')); + dispatch(changeSetting(['onboarded'], true)); + dispatch(saveSettings()); + } + }; +}; diff --git a/app/javascript/flavours/glitch/actions/pin_statuses.js b/app/javascript/flavours/glitch/actions/pin_statuses.js new file mode 100644 index 000000000..d3d1a154f --- /dev/null +++ b/app/javascript/flavours/glitch/actions/pin_statuses.js @@ -0,0 +1,40 @@ +import api from 'flavours/glitch/util/api'; + +export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST'; +export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS'; +export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL'; + +import { me } from 'flavours/glitch/util/initial_state'; + +export function fetchPinnedStatuses() { + return (dispatch, getState) => { + dispatch(fetchPinnedStatusesRequest()); + + api(getState).get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => { + dispatch(fetchPinnedStatusesSuccess(response.data, null)); + }).catch(error => { + dispatch(fetchPinnedStatusesFail(error)); + }); + }; +}; + +export function fetchPinnedStatusesRequest() { + return { + type: PINNED_STATUSES_FETCH_REQUEST, + }; +}; + +export function fetchPinnedStatusesSuccess(statuses, next) { + return { + type: PINNED_STATUSES_FETCH_SUCCESS, + statuses, + next, + }; +}; + +export function fetchPinnedStatusesFail(error) { + return { + type: PINNED_STATUSES_FETCH_FAIL, + error, + }; +}; diff --git a/app/javascript/flavours/glitch/actions/push_notifications.js b/app/javascript/flavours/glitch/actions/push_notifications.js new file mode 100644 index 000000000..55661d2b0 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/push_notifications.js @@ -0,0 +1,52 @@ +import axios from 'axios'; + +export const SET_BROWSER_SUPPORT = 'PUSH_NOTIFICATIONS_SET_BROWSER_SUPPORT'; +export const SET_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_SET_SUBSCRIPTION'; +export const CLEAR_SUBSCRIPTION = 'PUSH_NOTIFICATIONS_CLEAR_SUBSCRIPTION'; +export const ALERTS_CHANGE = 'PUSH_NOTIFICATIONS_ALERTS_CHANGE'; + +export function setBrowserSupport (value) { + return { + type: SET_BROWSER_SUPPORT, + value, + }; +} + +export function setSubscription (subscription) { + return { + type: SET_SUBSCRIPTION, + subscription, + }; +} + +export function clearSubscription () { + return { + type: CLEAR_SUBSCRIPTION, + }; +} + +export function changeAlerts(key, value) { + return dispatch => { + dispatch({ + type: ALERTS_CHANGE, + key, + value, + }); + + dispatch(saveSettings()); + }; +} + +export function saveSettings() { + return (_, getState) => { + const state = getState().get('push_notifications'); + const subscription = state.get('subscription'); + const alerts = state.get('alerts'); + + axios.put(`/api/web/push_subscriptions/${subscription.get('id')}`, { + data: { + alerts, + }, + }); + }; +} diff --git a/app/javascript/flavours/glitch/actions/reports.js b/app/javascript/flavours/glitch/actions/reports.js new file mode 100644 index 000000000..ad4fd18a9 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/reports.js @@ -0,0 +1,80 @@ +import api from 'flavours/glitch/util/api'; +import { openModal, closeModal } from './modal'; + +export const REPORT_INIT = 'REPORT_INIT'; +export const REPORT_CANCEL = 'REPORT_CANCEL'; + +export const REPORT_SUBMIT_REQUEST = 'REPORT_SUBMIT_REQUEST'; +export const REPORT_SUBMIT_SUCCESS = 'REPORT_SUBMIT_SUCCESS'; +export const REPORT_SUBMIT_FAIL = 'REPORT_SUBMIT_FAIL'; + +export const REPORT_STATUS_TOGGLE = 'REPORT_STATUS_TOGGLE'; +export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE'; + +export function initReport(account, status) { + return dispatch => { + dispatch({ + type: REPORT_INIT, + account, + status, + }); + + dispatch(openModal('REPORT')); + }; +}; + +export function cancelReport() { + return { + type: REPORT_CANCEL, + }; +}; + +export function toggleStatusReport(statusId, checked) { + return { + type: REPORT_STATUS_TOGGLE, + statusId, + checked, + }; +}; + +export function submitReport() { + return (dispatch, getState) => { + dispatch(submitReportRequest()); + + api(getState).post('/api/v1/reports', { + account_id: getState().getIn(['reports', 'new', 'account_id']), + status_ids: getState().getIn(['reports', 'new', 'status_ids']), + comment: getState().getIn(['reports', 'new', 'comment']), + }).then(response => { + dispatch(closeModal()); + dispatch(submitReportSuccess(response.data)); + }).catch(error => dispatch(submitReportFail(error))); + }; +}; + +export function submitReportRequest() { + return { + type: REPORT_SUBMIT_REQUEST, + }; +}; + +export function submitReportSuccess(report) { + return { + type: REPORT_SUBMIT_SUCCESS, + report, + }; +}; + +export function submitReportFail(error) { + return { + type: REPORT_SUBMIT_FAIL, + error, + }; +}; + +export function changeReportComment(comment) { + return { + type: REPORT_COMMENT_CHANGE, + comment, + }; +}; diff --git a/app/javascript/flavours/glitch/actions/search.js b/app/javascript/flavours/glitch/actions/search.js new file mode 100644 index 000000000..e86bd848e --- /dev/null +++ b/app/javascript/flavours/glitch/actions/search.js @@ -0,0 +1,73 @@ +import api from 'flavours/glitch/util/api'; + +export const SEARCH_CHANGE = 'SEARCH_CHANGE'; +export const SEARCH_CLEAR = 'SEARCH_CLEAR'; +export const SEARCH_SHOW = 'SEARCH_SHOW'; + +export const SEARCH_FETCH_REQUEST = 'SEARCH_FETCH_REQUEST'; +export const SEARCH_FETCH_SUCCESS = 'SEARCH_FETCH_SUCCESS'; +export const SEARCH_FETCH_FAIL = 'SEARCH_FETCH_FAIL'; + +export function changeSearch(value) { + return { + type: SEARCH_CHANGE, + value, + }; +}; + +export function clearSearch() { + return { + type: SEARCH_CLEAR, + }; +}; + +export function submitSearch() { + return (dispatch, getState) => { + const value = getState().getIn(['search', 'value']); + + if (value.length === 0) { + return; + } + + dispatch(fetchSearchRequest()); + + api(getState).get('/api/v1/search', { + params: { + q: value, + resolve: true, + }, + }).then(response => { + dispatch(fetchSearchSuccess(response.data)); + }).catch(error => { + dispatch(fetchSearchFail(error)); + }); + }; +}; + +export function fetchSearchRequest() { + return { + type: SEARCH_FETCH_REQUEST, + }; +}; + +export function fetchSearchSuccess(results) { + return { + type: SEARCH_FETCH_SUCCESS, + results, + accounts: results.accounts, + statuses: results.statuses, + }; +}; + +export function fetchSearchFail(error) { + return { + type: SEARCH_FETCH_FAIL, + error, + }; +}; + +export function showSearch() { + return { + type: SEARCH_SHOW, + }; +}; diff --git a/app/javascript/flavours/glitch/actions/settings.js b/app/javascript/flavours/glitch/actions/settings.js new file mode 100644 index 000000000..79adca18c --- /dev/null +++ b/app/javascript/flavours/glitch/actions/settings.js @@ -0,0 +1,31 @@ +import axios from 'axios'; +import { debounce } from 'lodash'; + +export const SETTING_CHANGE = 'SETTING_CHANGE'; +export const SETTING_SAVE = 'SETTING_SAVE'; + +export function changeSetting(key, value) { + return dispatch => { + dispatch({ + type: SETTING_CHANGE, + key, + value, + }); + + dispatch(saveSettings()); + }; +}; + +const debouncedSave = debounce((dispatch, getState) => { + if (getState().getIn(['settings', 'saved'])) { + return; + } + + const data = getState().get('settings').filter((_, key) => key !== 'saved').toJS(); + + axios.put('/api/web/settings', { data }).then(() => dispatch({ type: SETTING_SAVE })); +}, 5000, { trailing: true }); + +export function saveSettings() { + return (dispatch, getState) => debouncedSave(dispatch, getState); +}; diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js new file mode 100644 index 000000000..8b49083ac --- /dev/null +++ b/app/javascript/flavours/glitch/actions/statuses.js @@ -0,0 +1,217 @@ +import api from 'flavours/glitch/util/api'; + +import { deleteFromTimelines } from './timelines'; +import { fetchStatusCard } from './cards'; + +export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; +export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS'; +export const STATUS_FETCH_FAIL = 'STATUS_FETCH_FAIL'; + +export const STATUS_DELETE_REQUEST = 'STATUS_DELETE_REQUEST'; +export const STATUS_DELETE_SUCCESS = 'STATUS_DELETE_SUCCESS'; +export const STATUS_DELETE_FAIL = 'STATUS_DELETE_FAIL'; + +export const CONTEXT_FETCH_REQUEST = 'CONTEXT_FETCH_REQUEST'; +export const CONTEXT_FETCH_SUCCESS = 'CONTEXT_FETCH_SUCCESS'; +export const CONTEXT_FETCH_FAIL = 'CONTEXT_FETCH_FAIL'; + +export const STATUS_MUTE_REQUEST = 'STATUS_MUTE_REQUEST'; +export const STATUS_MUTE_SUCCESS = 'STATUS_MUTE_SUCCESS'; +export const STATUS_MUTE_FAIL = 'STATUS_MUTE_FAIL'; + +export const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST'; +export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS'; +export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL'; + +export function fetchStatusRequest(id, skipLoading) { + return { + type: STATUS_FETCH_REQUEST, + id, + skipLoading, + }; +}; + +export function fetchStatus(id) { + return (dispatch, getState) => { + const skipLoading = getState().getIn(['statuses', id], null) !== null; + + dispatch(fetchContext(id)); + dispatch(fetchStatusCard(id)); + + if (skipLoading) { + return; + } + + dispatch(fetchStatusRequest(id, skipLoading)); + + api(getState).get(`/api/v1/statuses/${id}`).then(response => { + dispatch(fetchStatusSuccess(response.data, skipLoading)); + }).catch(error => { + dispatch(fetchStatusFail(id, error, skipLoading)); + }); + }; +}; + +export function fetchStatusSuccess(status, skipLoading) { + return { + type: STATUS_FETCH_SUCCESS, + status, + skipLoading, + }; +}; + +export function fetchStatusFail(id, error, skipLoading) { + return { + type: STATUS_FETCH_FAIL, + id, + error, + skipLoading, + skipAlert: true, + }; +}; + +export function deleteStatus(id) { + return (dispatch, getState) => { + dispatch(deleteStatusRequest(id)); + + api(getState).delete(`/api/v1/statuses/${id}`).then(() => { + dispatch(deleteStatusSuccess(id)); + dispatch(deleteFromTimelines(id)); + }).catch(error => { + dispatch(deleteStatusFail(id, error)); + }); + }; +}; + +export function deleteStatusRequest(id) { + return { + type: STATUS_DELETE_REQUEST, + id: id, + }; +}; + +export function deleteStatusSuccess(id) { + return { + type: STATUS_DELETE_SUCCESS, + id: id, + }; +}; + +export function deleteStatusFail(id, error) { + return { + type: STATUS_DELETE_FAIL, + id: id, + error: error, + }; +}; + +export function fetchContext(id) { + return (dispatch, getState) => { + dispatch(fetchContextRequest(id)); + + api(getState).get(`/api/v1/statuses/${id}/context`).then(response => { + dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants)); + + }).catch(error => { + if (error.response && error.response.status === 404) { + dispatch(deleteFromTimelines(id)); + } + + dispatch(fetchContextFail(id, error)); + }); + }; +}; + +export function fetchContextRequest(id) { + return { + type: CONTEXT_FETCH_REQUEST, + id, + }; +}; + +export function fetchContextSuccess(id, ancestors, descendants) { + return { + type: CONTEXT_FETCH_SUCCESS, + id, + ancestors, + descendants, + statuses: ancestors.concat(descendants), + }; +}; + +export function fetchContextFail(id, error) { + return { + type: CONTEXT_FETCH_FAIL, + id, + error, + skipAlert: true, + }; +}; + +export function muteStatus(id) { + return (dispatch, getState) => { + dispatch(muteStatusRequest(id)); + + api(getState).post(`/api/v1/statuses/${id}/mute`).then(() => { + dispatch(muteStatusSuccess(id)); + }).catch(error => { + dispatch(muteStatusFail(id, error)); + }); + }; +}; + +export function muteStatusRequest(id) { + return { + type: STATUS_MUTE_REQUEST, + id, + }; +}; + +export function muteStatusSuccess(id) { + return { + type: STATUS_MUTE_SUCCESS, + id, + }; +}; + +export function muteStatusFail(id, error) { + return { + type: STATUS_MUTE_FAIL, + id, + error, + }; +}; + +export function unmuteStatus(id) { + return (dispatch, getState) => { + dispatch(unmuteStatusRequest(id)); + + api(getState).post(`/api/v1/statuses/${id}/unmute`).then(() => { + dispatch(unmuteStatusSuccess(id)); + }).catch(error => { + dispatch(unmuteStatusFail(id, error)); + }); + }; +}; + +export function unmuteStatusRequest(id) { + return { + type: STATUS_UNMUTE_REQUEST, + id, + }; +}; + +export function unmuteStatusSuccess(id) { + return { + type: STATUS_UNMUTE_SUCCESS, + id, + }; +}; + +export function unmuteStatusFail(id, error) { + return { + type: STATUS_UNMUTE_FAIL, + id, + error, + }; +}; diff --git a/app/javascript/flavours/glitch/actions/store.js b/app/javascript/flavours/glitch/actions/store.js new file mode 100644 index 000000000..a1db0fdd5 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/store.js @@ -0,0 +1,17 @@ +import { Iterable, fromJS } from 'immutable'; + +export const STORE_HYDRATE = 'STORE_HYDRATE'; +export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY'; + +const convertState = rawState => + fromJS(rawState, (k, v) => + Iterable.isIndexed(v) ? v.toList() : v.toMap()); + +export function hydrateStore(rawState) { + const state = convertState(rawState); + + return { + type: STORE_HYDRATE, + state, + }; +}; diff --git a/app/javascript/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js new file mode 100644 index 000000000..595eefa41 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/streaming.js @@ -0,0 +1,54 @@ +import { connectStream } from 'flavours/glitch/util/stream'; +import { + updateTimeline, + deleteFromTimelines, + refreshHomeTimeline, + connectTimeline, + disconnectTimeline, +} from './timelines'; +import { updateNotifications, refreshNotifications } from './notifications'; +import { getLocale } from 'mastodon/locales'; + +const { messages } = getLocale(); + +export function connectTimelineStream (timelineId, path, pollingRefresh = null) { + + return connectStream (path, pollingRefresh, (dispatch, getState) => { + const locale = getState().getIn(['meta', 'locale']); + return { + onConnect() { + dispatch(connectTimeline(timelineId)); + }, + + onDisconnect() { + dispatch(disconnectTimeline(timelineId)); + }, + + onReceive (data) { + switch(data.event) { + case 'update': + dispatch(updateTimeline(timelineId, JSON.parse(data.payload))); + break; + case 'delete': + dispatch(deleteFromTimelines(data.payload)); + break; + case 'notification': + dispatch(updateNotifications(JSON.parse(data.payload), messages, locale)); + break; + } + }, + }; + }); +} + +function refreshHomeTimelineAndNotification (dispatch) { + dispatch(refreshHomeTimeline()); + dispatch(refreshNotifications()); +} + +export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification); +export const connectCommunityStream = () => connectTimelineStream('community', 'public:local'); +export const connectMediaStream = () => connectTimelineStream('community', 'public:local'); +export const connectPublicStream = () => connectTimelineStream('public', 'public'); +export const connectHashtagStream = (tag) => connectTimelineStream(`hashtag:${tag}`, `hashtag&tag=${tag}`); +export const connectDirectStream = () => connectTimelineStream('direct', 'direct'); diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js new file mode 100644 index 000000000..3fabf3885 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -0,0 +1,208 @@ +import api, { getLinks } from 'flavours/glitch/util/api'; +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; + +export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; +export const TIMELINE_DELETE = 'TIMELINE_DELETE'; + +export const TIMELINE_REFRESH_REQUEST = 'TIMELINE_REFRESH_REQUEST'; +export const TIMELINE_REFRESH_SUCCESS = 'TIMELINE_REFRESH_SUCCESS'; +export const TIMELINE_REFRESH_FAIL = 'TIMELINE_REFRESH_FAIL'; + +export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST'; +export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS'; +export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; + +export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; + +export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; +export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; + +export const TIMELINE_CONTEXT_UPDATE = 'CONTEXT_UPDATE'; + +export function refreshTimelineSuccess(timeline, statuses, skipLoading, next) { + return { + type: TIMELINE_REFRESH_SUCCESS, + timeline, + statuses, + skipLoading, + next, + }; +}; + +export function updateTimeline(timeline, status) { + return (dispatch, getState) => { + const references = status.reblog ? getState().get('statuses').filter((item, itemId) => (itemId === status.reblog.id || item.get('reblog') === status.reblog.id)).map((_, itemId) => itemId) : []; + const parents = []; + + if (status.in_reply_to_id) { + let parent = getState().getIn(['statuses', status.in_reply_to_id]); + + while (parent && parent.get('in_reply_to_id')) { + parents.push(parent.get('id')); + parent = getState().getIn(['statuses', parent.get('in_reply_to_id')]); + } + } + + dispatch({ + type: TIMELINE_UPDATE, + timeline, + status, + references, + }); + + if (parents.length > 0) { + dispatch({ + type: TIMELINE_CONTEXT_UPDATE, + status, + references: parents, + }); + } + }; +}; + +export function deleteFromTimelines(id) { + return (dispatch, getState) => { + const accountId = getState().getIn(['statuses', id, 'account']); + const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => [status.get('id'), status.get('account')]); + const reblogOf = getState().getIn(['statuses', id, 'reblog'], null); + + dispatch({ + type: TIMELINE_DELETE, + id, + accountId, + references, + reblogOf, + }); + }; +}; + +export function refreshTimelineRequest(timeline, skipLoading) { + return { + type: TIMELINE_REFRESH_REQUEST, + timeline, + skipLoading, + }; +}; + +export function refreshTimeline(timelineId, path, params = {}) { + return function (dispatch, getState) { + const timeline = getState().getIn(['timelines', timelineId], ImmutableMap()); + + if (timeline.get('isLoading') || timeline.get('online')) { + return; + } + + const ids = timeline.get('items', ImmutableList()); + const newestId = ids.size > 0 ? ids.first() : null; + + let skipLoading = timeline.get('loaded'); + + if (newestId !== null) { + params.since_id = newestId; + } + + dispatch(refreshTimelineRequest(timelineId, skipLoading)); + + api(getState).get(path, { params }).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(refreshTimelineSuccess(timelineId, response.data, skipLoading, next ? next.uri : null)); + }).catch(error => { + dispatch(refreshTimelineFail(timelineId, error, skipLoading)); + }); + }; +}; + +export const refreshHomeTimeline = () => refreshTimeline('home', '/api/v1/timelines/home'); +export const refreshPublicTimeline = () => refreshTimeline('public', '/api/v1/timelines/public'); +export const refreshCommunityTimeline = () => refreshTimeline('community', '/api/v1/timelines/public', { local: true }); +export const refreshDirectTimeline = () => refreshTimeline('direct', '/api/v1/timelines/direct'); +export const refreshAccountTimeline = accountId => refreshTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`); +export const refreshAccountMediaTimeline = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true }); +export const refreshHashtagTimeline = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`); + +export function refreshTimelineFail(timeline, error, skipLoading) { + return { + type: TIMELINE_REFRESH_FAIL, + timeline, + error, + skipLoading, + skipAlert: error.response && error.response.status === 404, + }; +}; + +export function expandTimeline(timelineId, path, params = {}) { + return (dispatch, getState) => { + const timeline = getState().getIn(['timelines', timelineId], ImmutableMap()); + const ids = timeline.get('items', ImmutableList()); + + if (timeline.get('isLoading') || ids.size === 0) { + return; + } + + params.max_id = ids.last(); + params.limit = 10; + + dispatch(expandTimelineRequest(timelineId)); + + api(getState).get(path, { params }).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null)); + }).catch(error => { + dispatch(expandTimelineFail(timelineId, error)); + }); + }; +}; + +export const expandHomeTimeline = () => expandTimeline('home', '/api/v1/timelines/home'); +export const expandPublicTimeline = () => expandTimeline('public', '/api/v1/timelines/public'); +export const expandCommunityTimeline = () => expandTimeline('community', '/api/v1/timelines/public', { local: true }); +export const expandDirectTimeline = () => expandTimeline('direct', '/api/v1/timelines/direct'); +export const expandAccountTimeline = accountId => expandTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`); +export const expandAccountMediaTimeline = accountId => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true }); +export const expandHashtagTimeline = hashtag => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`); + +export function expandTimelineRequest(timeline) { + return { + type: TIMELINE_EXPAND_REQUEST, + timeline, + }; +}; + +export function expandTimelineSuccess(timeline, statuses, next) { + return { + type: TIMELINE_EXPAND_SUCCESS, + timeline, + statuses, + next, + }; +}; + +export function expandTimelineFail(timeline, error) { + return { + type: TIMELINE_EXPAND_FAIL, + timeline, + error, + }; +}; + +export function scrollTopTimeline(timeline, top) { + return { + type: TIMELINE_SCROLL_TOP, + timeline, + top, + }; +}; + +export function connectTimeline(timeline) { + return { + type: TIMELINE_CONNECT, + timeline, + }; +}; + +export function disconnectTimeline(timeline) { + return { + type: TIMELINE_DISCONNECT, + timeline, + }; +}; diff --git a/app/javascript/flavours/glitch/components/account.js b/app/javascript/flavours/glitch/components/account.js new file mode 100644 index 000000000..c8dacb0ab --- /dev/null +++ b/app/javascript/flavours/glitch/components/account.js @@ -0,0 +1,116 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import Avatar from './avatar'; +import DisplayName from './display_name'; +import Permalink from './permalink'; +import IconButton from './icon_button'; +import { defineMessages, injectIntl } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { me } from 'flavours/glitch/util/initial_state'; + +const messages = defineMessages({ + follow: { id: 'account.follow', defaultMessage: 'Follow' }, + unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, + requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, + unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, + unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, + mute_notifications: { id: 'account.mute_notifications', defaultMessage: 'You are not currently muting notifications from @{name}. Click to mute notifications' }, + unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'You are currently muting notifications from @{name}. Click to unmute notifications' }, +}); + +@injectIntl +export default class Account extends ImmutablePureComponent { + + static propTypes = { + account: ImmutablePropTypes.map.isRequired, + onFollow: PropTypes.func.isRequired, + onBlock: PropTypes.func.isRequired, + onMute: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + hidden: PropTypes.bool, + }; + + handleFollow = () => { + this.props.onFollow(this.props.account); + } + + handleBlock = () => { + this.props.onBlock(this.props.account); + } + + handleMute = () => { + this.props.onMute(this.props.account); + } + + handleMuteNotifications = () => { + this.props.onMuteNotifications(this.props.account, true); + } + + handleUnmuteNotifications = () => { + this.props.onMuteNotifications(this.props.account, false); + } + + render () { + const { account, intl, hidden } = this.props; + + if (!account) { + return
; + } + + if (hidden) { + return ( +
+ {account.get('display_name')} + {account.get('username')} +
+ ); + } + + let buttons; + + if (account.get('id') !== me && account.get('relationship', null) !== null) { + const following = account.getIn(['relationship', 'following']); + const requested = account.getIn(['relationship', 'requested']); + const blocking = account.getIn(['relationship', 'blocking']); + const muting = account.getIn(['relationship', 'muting']); + + if (requested) { + buttons = ; + } else if (blocking) { + buttons = ; + } else if (muting) { + let hidingNotificationsButton; + if (muting.get('notifications')) { + hidingNotificationsButton = ; + } else { + hidingNotificationsButton = ; + } + buttons = ( +
+ + {hidingNotificationsButton} +
+ ); + } else { + buttons = ; + } + } + + return ( +
+
+ +
+ +
+ +
+ {buttons} +
+
+
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/components/attachment_list.js b/app/javascript/flavours/glitch/components/attachment_list.js new file mode 100644 index 000000000..b3d00b335 --- /dev/null +++ b/app/javascript/flavours/glitch/components/attachment_list.js @@ -0,0 +1,33 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +const filename = url => url.split('/').pop().split('#')[0].split('?')[0]; + +export default class AttachmentList extends ImmutablePureComponent { + + static propTypes = { + media: ImmutablePropTypes.list.isRequired, + }; + + render () { + const { media } = this.props; + + return ( +
+
+ +
+ + +
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/components/autosuggest_emoji.js b/app/javascript/flavours/glitch/components/autosuggest_emoji.js new file mode 100644 index 000000000..79e113d9c --- /dev/null +++ b/app/javascript/flavours/glitch/components/autosuggest_emoji.js @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import unicodeMapping from 'flavours/glitch/util/emoji/emoji_unicode_mapping_light'; + +const assetHost = process.env.CDN_HOST || ''; + +export default class AutosuggestEmoji extends React.PureComponent { + + static propTypes = { + emoji: PropTypes.object.isRequired, + }; + + render () { + const { emoji } = this.props; + let url; + + if (emoji.custom) { + url = emoji.imageUrl; + } else { + const mapping = unicodeMapping[emoji.native] || unicodeMapping[emoji.native.replace(/\uFE0F$/, '')]; + + if (!mapping) { + return null; + } + + url = `${assetHost}/emoji/${mapping.filename}.svg`; + } + + return ( +
+ {emoji.native + + {emoji.colons} +
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/components/autosuggest_textarea.js b/app/javascript/flavours/glitch/components/autosuggest_textarea.js new file mode 100644 index 000000000..551528e5a --- /dev/null +++ b/app/javascript/flavours/glitch/components/autosuggest_textarea.js @@ -0,0 +1,222 @@ +import React from 'react'; +import AutosuggestAccountContainer from 'flavours/glitch/features/compose/containers/autosuggest_account_container'; +import AutosuggestEmoji from './autosuggest_emoji'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import { isRtl } from 'flavours/glitch/util/rtl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import Textarea from 'react-textarea-autosize'; +import classNames from 'classnames'; + +const textAtCursorMatchesToken = (str, caretPosition) => { + let word; + + let left = str.slice(0, caretPosition).search(/[^\s\u200B]+$/); + let right = str.slice(caretPosition).search(/[\s\u200B]/); + + if (right < 0) { + word = str.slice(left); + } else { + word = str.slice(left, right + caretPosition); + } + + if (!word || word.trim().length < 3 || ['@', ':'].indexOf(word[0]) === -1) { + return [null, null]; + } + + word = word.trim().toLowerCase(); + + if (word.length > 0) { + return [left + 1, word]; + } else { + return [null, null]; + } +}; + +export default class AutosuggestTextarea extends ImmutablePureComponent { + + static propTypes = { + value: PropTypes.string, + suggestions: ImmutablePropTypes.list, + disabled: PropTypes.bool, + placeholder: PropTypes.string, + onSuggestionSelected: PropTypes.func.isRequired, + onSuggestionsClearRequested: PropTypes.func.isRequired, + onSuggestionsFetchRequested: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + onKeyUp: PropTypes.func, + onKeyDown: PropTypes.func, + onPaste: PropTypes.func.isRequired, + autoFocus: PropTypes.bool, + }; + + static defaultProps = { + autoFocus: true, + }; + + state = { + suggestionsHidden: false, + selectedSuggestion: 0, + lastToken: null, + tokenStart: 0, + }; + + onChange = (e) => { + const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart); + + if (token !== null && this.state.lastToken !== token) { + this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart }); + this.props.onSuggestionsFetchRequested(token); + } else if (token === null) { + this.setState({ lastToken: null }); + this.props.onSuggestionsClearRequested(); + } + + this.props.onChange(e); + } + + onKeyDown = (e) => { + const { suggestions, disabled } = this.props; + const { selectedSuggestion, suggestionsHidden } = this.state; + + if (disabled) { + e.preventDefault(); + return; + } + + switch(e.key) { + case 'Escape': + if (!suggestionsHidden) { + e.preventDefault(); + this.setState({ suggestionsHidden: true }); + } + + break; + case 'ArrowDown': + if (suggestions.size > 0 && !suggestionsHidden) { + e.preventDefault(); + this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) }); + } + + break; + case 'ArrowUp': + if (suggestions.size > 0 && !suggestionsHidden) { + e.preventDefault(); + this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) }); + } + + break; + case 'Enter': + case 'Tab': + // Select suggestion + if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) { + e.preventDefault(); + e.stopPropagation(); + this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion)); + } + + break; + } + + if (e.defaultPrevented || !this.props.onKeyDown) { + return; + } + + this.props.onKeyDown(e); + } + + onKeyUp = e => { + if (e.key === 'Escape' && this.state.suggestionsHidden) { + document.querySelector('.ui').parentElement.focus(); + } + + if (this.props.onKeyUp) { + this.props.onKeyUp(e); + } + } + + onBlur = () => { + this.setState({ suggestionsHidden: true }); + } + + onSuggestionClick = (e) => { + const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index')); + e.preventDefault(); + this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion); + this.textarea.focus(); + } + + componentWillReceiveProps (nextProps) { + if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden) { + this.setState({ suggestionsHidden: false }); + } + } + + setTextarea = (c) => { + this.textarea = c; + } + + onPaste = (e) => { + if (e.clipboardData && e.clipboardData.files.length === 1) { + this.props.onPaste(e.clipboardData.files); + e.preventDefault(); + } + } + + renderSuggestion = (suggestion, i) => { + const { selectedSuggestion } = this.state; + let inner, key; + + if (typeof suggestion === 'object') { + inner = ; + key = suggestion.id; + } else { + inner = ; + key = suggestion; + } + + return ( +
+ {inner} +
+ ); + } + + render () { + const { value, suggestions, disabled, placeholder, autoFocus } = this.props; + const { suggestionsHidden } = this.state; + const style = { direction: 'ltr' }; + + if (isRtl(value)) { + style.direction = 'rtl'; + } + + return ( +
+