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 --- config/webpack/configuration.js | 13 +++++++++++- config/webpack/generateLocalePacks.js | 2 +- config/webpack/shared.js | 40 +++++++++++++++++++---------------- 3 files changed, 35 insertions(+), 20 deletions(-) (limited to 'config/webpack') 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. }), ], -- cgit From 8812bab6875024f76c59ab43d1dd3717e5e6da68 Mon Sep 17 00:00:00 2001 From: kibigo! Date: Tue, 21 Nov 2017 18:17:38 -0800 Subject: Minor fixes --- app/javascript/themes/win95/theme.yml | 5 ----- app/views/layouts/embedded.html.haml | 3 +-- config/webpack/configuration.js | 2 +- 3 files changed, 2 insertions(+), 8 deletions(-) (limited to 'config/webpack') diff --git a/app/javascript/themes/win95/theme.yml b/app/javascript/themes/win95/theme.yml index 43af38198..c4ac8aa55 100644 --- a/app/javascript/themes/win95/theme.yml +++ b/app/javascript/themes/win95/theme.yml @@ -13,11 +13,6 @@ pack: 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/views/layouts/embedded.html.haml b/app/views/layouts/embedded.html.haml index 3960167bf..935670514 100644 --- a/app/views/layouts/embedded.html.haml +++ b/app/views/layouts/embedded.html.haml @@ -4,8 +4,7 @@ %meta{ charset: 'utf-8' }/ %meta{ name: 'robots', content: 'noindex' }/ - = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous' - = javascript_pack_tag 'embed', integrity: true, crossorigin: 'anonymous' + = javascript_pack_tag "locales", integrity: true, crossorigin: 'anonymous' = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous' = render partial: 'layouts/theme', object: @core = render partial: 'layouts/theme', object: @theme diff --git a/config/webpack/configuration.js b/config/webpack/configuration.js index 9514bc547..f8741c5d8 100644 --- a/config/webpack/configuration.js +++ b/config/webpack/configuration.js @@ -28,7 +28,7 @@ for (let i = 0; i < themeFiles.length; i++) { if (!data.pack_directory) { data.pack_directory = dirname(themeFile); } - if (data.pack) { + if (data.pack && typeof data.pack == 'object') { themes[data.name] = data; } } -- 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 'config/webpack') 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 'config/webpack') 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 ( +
+