diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index f5d5c1244..6cb1b64e8 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -55,6 +55,8 @@ class Settings::PreferencesController < Settings::BaseController :setting_trends, :setting_crop_images, :setting_always_send_emails, + :setting_place_tab_bar_at_bottom, + :setting_show_tab_bar_label, notification_emails: %i(follow follow_request reblog favourite mention report pending_account trending_tag appeal), interactions: %i(must_be_follower must_be_following must_be_following_dm) ) diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index 1dd6e34e8..340bf6e3e 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -21,8 +21,15 @@ import { } from '../../ui/util/async-components'; import ComposePanel from './compose_panel'; import NavigationPanel from './navigation_panel'; +import TabsBar from './tabs_bar'; +import { place_tab_bar_at_bottom } from 'mastodon/initial_state'; import { supportsPassiveEvents } from 'detect-passive-events'; import { scrollRight } from '../../../scroll'; +import { Link } from 'react-router-dom'; +import classNames from 'classnames'; +import Icon from 'mastodon/components/icon'; + +const shouldHideFAB = path => path.match(/^\/statuses\/|^\/search|^\/getting-started/); const componentMap = { 'COMPOSE': Compose, @@ -138,26 +145,49 @@ export default class ColumnsArea extends ImmutablePureComponent { const { renderComposePanel } = this.state; if (singleColumn) { - return ( -
-
-
- {renderComposePanel && } + if (place_tab_bar_at_bottom) { + return ( +
+
+
+ {renderComposePanel && } +
+
+ +
+
+
{children}
+
+ +
+ {location.pathname !== '/publish' && } + +
+ +
+ ); + } else { + return ( +
+
+
+ {renderComposePanel && } +
+
+ +
+
+
{children}
+
+ +
+
+ +
- -
-
-
{children}
-
- -
-
- -
-
-
- ); + ); + } } return ( diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js new file mode 100644 index 000000000..e919a1e25 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/tabs_bar.js @@ -0,0 +1,96 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { NavLink, withRouter } from 'react-router-dom'; +import { FormattedMessage, injectIntl } from 'react-intl'; +import NotificationsCounterIcon from './notifications_counter_icon'; +import { place_tab_bar_at_bottom, show_tab_bar_label } from 'mastodon/initial_state'; +import classNames from 'classnames'; +import { debounce } from 'lodash'; +import { isUserTouching } from '../../../is_mobile'; +import Icon from 'mastodon/components/icon'; + +export const links = [ + , + , + , + , + , + , +]; + +export function getIndex (path) { + return links.findIndex(link => link.props.to === path); +} + +export function getLink (index) { + return links[index].props.to; +} + +export default @injectIntl +@withRouter +class TabsBar extends React.PureComponent { + + static propTypes = { + intl: PropTypes.object.isRequired, + history: PropTypes.object.isRequired, + }; + + setRef = ref => { + this.node = ref; + } + + handleClick = (e) => { + // Only apply optimization for touch devices, which we assume are slower + // We thus avoid the 250ms delay for non-touch devices and the lag for touch devices + if (isUserTouching()) { + e.preventDefault(); + e.persist(); + + requestAnimationFrame(() => { + const tabs = Array(...this.node.querySelectorAll('.tabs-bar__link')); + const currentTab = tabs.find(tab => tab.classList.contains('active')); + const nextTab = tabs.find(tab => tab.contains(e.target)); + const { props: { to } } = links[Array(...this.node.childNodes).indexOf(nextTab)]; + + + if (currentTab !== nextTab) { + if (currentTab) { + currentTab.classList.remove('active'); + } + + const listener = debounce(() => { + nextTab.removeEventListener('transitionend', listener); + this.props.history.push(to); + }, 50); + + nextTab.addEventListener('transitionend', listener); + nextTab.classList.add('active'); + } + }); + } + + } + + static contextTypes = { + router: PropTypes.object.isRequired, + identity: PropTypes.object.isRequired, + }; + + static propTypes = { + intl: PropTypes.object.isRequired, + }; + + render () { + const { intl: { formatMessage } } = this.props; + return ( +
+ + +
+
+ ); + } + +} \ No newline at end of file diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index d04c4a42d..3477913f5 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -135,5 +135,7 @@ export const version = getMeta('version'); export const translationEnabled = getMeta('translation_enabled'); export const languages = initialState?.languages; export const statusPageUrl = getMeta('status_page_url'); +export const place_tab_bar_at_bottom = getMeta('place_tab_bar_at_bottom'); +export const show_tab_bar_label = getMeta('show_tab_bar_label'); export default initialState; diff --git a/app/javascript/styles/application.scss b/app/javascript/styles/application.scss index 81a040108..e30e42304 100644 --- a/app/javascript/styles/application.scss +++ b/app/javascript/styles/application.scss @@ -23,3 +23,5 @@ @import 'mastodon/dashboard'; @import 'mastodon/rtl'; @import 'mastodon/accessibility'; + +@import 'plugin'; \ No newline at end of file diff --git a/app/javascript/styles/plugin.scss b/app/javascript/styles/plugin.scss new file mode 100644 index 000000000..bbeab2555 --- /dev/null +++ b/app/javascript/styles/plugin.scss @@ -0,0 +1,34 @@ +// ここから下タブバーの実装 + +//投稿ボタン +.columns-area__panels__main .button.bottom_right { + position: fixed; + right: 18px; + bottom: 65px; + display: flex; + align-items: center; + justify-content: center; + width: 64px; + height: 64px; + font-size: 24px; + border-radius: 50%; +} + +.tab-ber-bottom .timeline{ + width:100%; + height: calc(100% - 55px); + margin: 0 0 50px 0; +} + +.tab-ber-bottom .navber{ + width:100%; + bottom: 0; + position: fixed; +} + +.tab-ber-bottom .navber .tabs-bar__wrapper{ + bottom: 0; + width: 100%; +} + +//ここまで下タブバーの実装 \ No newline at end of file diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index 5fb7655a9..5a919e3d4 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -39,6 +39,7 @@ class UserSettingsDecorator user.settings['trends'] = trends_preference if change?('setting_trends') user.settings['crop_images'] = crop_images_preference if change?('setting_crop_images') user.settings['always_send_emails'] = always_send_emails_preference if change?('setting_always_send_emails') + user.settings['place_tab_bar_at_bottom'] = place_tab_bar_at_bottom_preference if change?('setting_place_tab_bar_at_bottom') end def merged_notification_emails @@ -137,6 +138,14 @@ class UserSettingsDecorator boolean_cast_setting 'setting_always_send_emails' end + def place_tab_bar_at_bottom_preference + boolean_cast_setting 'setting_place_tab_bar_at_bottom' + end + + def show_tab_bar_label_preference + boolean_cast_setting 'setting_show_tab_bar_label' + end + def boolean_cast_setting(key) ActiveModel::Type::Boolean.new.cast(settings[key]) end diff --git a/app/models/user.rb b/app/models/user.rb index efbe29f44..f03700783 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -136,6 +136,7 @@ class User < ApplicationRecord :expand_spoilers, :default_language, :aggregate_reblogs, :show_application, :advanced_layout, :use_blurhash, :use_pending_items, :trends, :crop_images, :disable_swiping, :always_send_emails, + :place_tab_bar_at_bottom, to: :settings, prefix: :setting, allow_nil: false delegate :can?, to: :role diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 24417bca7..0f6ae274b 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -51,6 +51,7 @@ class InitialStateSerializer < ActiveModel::Serializer store[:use_pending_items] = object.current_account.user.setting_use_pending_items store[:trends] = Setting.trends && object.current_account.user.setting_trends store[:crop_images] = object.current_account.user.setting_crop_images + store[:place_tab_bar_at_bottom] = object.current_account.user.setting_place_tab_bar_at_bottom else store[:auto_play_gif] = Setting.auto_play_gif store[:display_media] = Setting.display_media diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 7b9434d6f..80bf51439 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -2,7 +2,7 @@ %html{ lang: I18n.locale } %head %meta{ charset: 'utf-8' }/ - %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1' }/ + %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1, viewport-fit=cover'}/ - if cdn_host? %link{ rel: 'dns-prefetch', href: cdn_host }/ diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml index 9e3964f21..74306146f 100644 --- a/app/views/settings/preferences/appearance/show.html.haml +++ b/app/views/settings/preferences/appearance/show.html.haml @@ -32,6 +32,7 @@ = f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label = f.input :setting_disable_swiping, as: :boolean, wrapper: :with_label = f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label + = f.input :setting_place_tab_bar_at_bottom, as: :boolean, wrapper: :with_label %h4= t 'appearance.toot_layout' diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 96b0131ef..d51557d77 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -56,6 +56,7 @@ en: setting_display_media_show_all: Always show media setting_hide_network: Who you follow and who follows you will be hidden on your profile setting_noindex: Affects your public profile and post pages + setting_place_tab_bar_at_bottom: Place the tab bar at the bottom setting_show_application: The application you use to post will be displayed in the detailed view of your posts setting_use_blurhash: Gradients are based on the colors of the hidden visuals but obfuscate any details setting_use_pending_items: Hide timeline updates behind a click instead of automatically scrolling the feed diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index ae73cefea..0ebd585fa 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -209,6 +209,7 @@ ja: setting_expand_spoilers: 閲覧注意としてマークされたトゥートを常に展開する setting_hide_network: 繋がりを隠す setting_noindex: 検索エンジンによるインデックスを拒否する + setting_place_tab_bar_at_bottom: タブバーを下に配置する setting_reduce_motion: アニメーションの動きを減らす setting_show_application: 送信したアプリを開示する setting_system_font_ui: システムのデフォルトフォントを使う diff --git a/config/settings.yml b/config/settings.yml index f0b09dd5c..5976685d7 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -38,6 +38,7 @@ defaults: &defaults trends_as_landing_page: true trendable_by_default: false crop_images: true + place_tab_bar_at_bottom: false notification_emails: follow: true reblog: false