diff --git a/app/javascript/mastodon/components/autosuggest_input.js b/app/javascript/mastodon/components/autosuggest_input.js
index bb8ab60db..4b4aa8f0e 100644
--- a/app/javascript/mastodon/components/autosuggest_input.js
+++ b/app/javascript/mastodon/components/autosuggest_input.js
@@ -49,7 +49,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
autoFocus: PropTypes.bool,
className: PropTypes.string,
id: PropTypes.string,
- searchTokens: PropTypes.list,
+ searchTokens: ImmutablePropTypes.list,
maxLength: PropTypes.number,
};
diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js
index fff329106..0731abcf4 100644
--- a/app/javascript/mastodon/features/compose/index.js
+++ b/app/javascript/mastodon/features/compose/index.js
@@ -106,12 +106,12 @@ class Compose extends React.PureComponent {
+
{view}
);
@@ -164,13 +164,17 @@ class ColumnsArea extends ImmutablePureComponent {
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null :
;
return columnIndex !== -1 ? [
+
,
+
{links.map(this.renderView)}
,
floatingActionButton,
] : [
-
{children}
,
+
,
+
+
{children}
,
floatingActionButton,
];
diff --git a/app/javascript/mastodon/features/ui/components/notifications_counter_icon.js b/app/javascript/mastodon/features/ui/components/notifications_counter_icon.js
new file mode 100644
index 000000000..deb907866
--- /dev/null
+++ b/app/javascript/mastodon/features/ui/components/notifications_counter_icon.js
@@ -0,0 +1,23 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import Icon from 'mastodon/components/icon';
+
+const mapStateToProps = state => ({
+ count: state.getIn(['notifications', 'unread']),
+});
+
+const formatNumber = num => num > 99 ? '99+' : num;
+
+const NotificationsCounterIcon = ({ count }) => (
+
+
+ {count > 0 && {formatNumber(count)}}
+
+);
+
+NotificationsCounterIcon.propTypes = {
+ count: PropTypes.number.isRequired,
+};
+
+export default connect(mapStateToProps)(NotificationsCounterIcon);
diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js
index 1b2bb7781..979b782bb 100644
--- a/app/javascript/mastodon/features/ui/components/tabs_bar.js
+++ b/app/javascript/mastodon/features/ui/components/tabs_bar.js
@@ -5,10 +5,11 @@ import { FormattedMessage, injectIntl } from 'react-intl';
import { debounce } from 'lodash';
import { isUserTouching } from '../../../is_mobile';
import Icon from 'mastodon/components/icon';
+import NotificationsCounterIcon from './notifications_counter_icon';
export const links = [
,
-
,
+
,
,
,
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index 1fcea779d..6d5279157 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -7,7 +7,6 @@ import { Redirect, withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import NotificationsContainer from './containers/notifications_container';
import LoadingBarContainer from './containers/loading_bar_container';
-import TabsBar from './components/tabs_bar';
import ModalContainer from './containers/modal_container';
import { isMobile } from '../../is_mobile';
import { debounce } from 'lodash';
@@ -63,6 +62,7 @@ const mapStateToProps = state => ({
hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0,
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
+ forceSingleColumn: state.getIn(['settings', 'forceSingleColumn'], false),
});
const keyMap = {
@@ -101,6 +101,7 @@ class SwitchingColumnsArea extends React.PureComponent {
children: PropTypes.node,
location: PropTypes.object,
onLayoutChange: PropTypes.func.isRequired,
+ forceSingleColumn: PropTypes.bool,
};
state = {
@@ -139,12 +140,13 @@ class SwitchingColumnsArea extends React.PureComponent {
}
render () {
- const { children } = this.props;
+ const { children, forceSingleColumn } = this.props;
const { mobile } = this.state;
- const redirect = mobile ?
:
;
+ const singleColumn = forceSingleColumn || mobile;
+ const redirect = singleColumn ?
:
;
return (
-
+
{redirect}
@@ -205,6 +207,7 @@ class UI extends React.PureComponent {
location: PropTypes.object,
intl: PropTypes.object.isRequired,
dropdownMenuIsOpen: PropTypes.bool,
+ forceSingleColumn: PropTypes.bool,
};
state = {
@@ -453,7 +456,7 @@ class UI extends React.PureComponent {
render () {
const { draggingOver } = this.state;
- const { children, isComposing, location, dropdownMenuIsOpen } = this.props;
+ const { children, isComposing, location, dropdownMenuIsOpen, forceSingleColumn } = this.props;
const handlers = {
help: this.handleHotkeyToggleHelp,
@@ -479,9 +482,7 @@ class UI extends React.PureComponent {
return (
-
-
-
+
{children}
diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js
index a0eea137f..419c313af 100644
--- a/app/javascript/mastodon/reducers/settings.js
+++ b/app/javascript/mastodon/reducers/settings.js
@@ -14,6 +14,8 @@ const initialState = ImmutableMap({
skinTone: 1,
+ forceSingleColumn: false,
+
home: ImmutableMap({
shows: ImmutableMap({
reblog: true,
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index ab466f944..9acf0acfc 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -1788,16 +1788,6 @@ a.account__display-name {
}
}
-@media screen and (min-width: 360px) {
- .columns-area {
- padding: 10px;
- }
-
- .react-swipeable-view-container .columns-area {
- height: calc(100% - 20px) !important;
- }
-}
-
.react-swipeable-view-container {
&,
.columns-area,
@@ -1860,36 +1850,6 @@ a.account__display-name {
overflow: hidden;
}
-@media screen and (min-width: 360px) {
- .tabs-bar {
- margin: 10px;
- margin-bottom: 0;
- }
-
- .getting-started__wrapper,
- .getting-started__trends,
- .search {
- margin-bottom: 10px;
- }
-}
-
-@media screen and (max-width: 630px) {
- .column,
- .drawer {
- width: 100%;
- padding: 0;
- }
-
- .columns-area {
- flex-direction: column;
- }
-
- .search__input,
- .autosuggest-textarea__textarea {
- font-size: 16px;
- }
-}
-
@media screen and (min-width: 631px) {
.columns-area {
padding: 0;
@@ -1920,76 +1880,8 @@ a.account__display-name {
}
}
-.drawer__pager {
- box-sizing: border-box;
- padding: 0;
- flex-grow: 1;
- position: relative;
- overflow: hidden;
- display: flex;
-}
-
-.drawer__inner {
- position: absolute;
- top: 0;
- left: 0;
- background: lighten($ui-base-color, 13%);
- box-sizing: border-box;
- padding: 0;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- overflow-y: auto;
- width: 100%;
- height: 100%;
-
- &.darker {
- background: $ui-base-color;
- }
-}
-
-.drawer__inner__mastodon {
- background: lighten($ui-base-color, 13%) url('data:image/svg+xml;utf8,') no-repeat bottom / 100% auto;
- flex: 1;
- min-height: 47px;
-
- > img {
- display: block;
- object-fit: contain;
- object-position: bottom left;
- width: 100%;
- height: 100%;
- pointer-events: none;
- user-drag: none;
- user-select: none;
- }
-}
-
-.pseudo-drawer {
- background: lighten($ui-base-color, 13%);
- font-size: 13px;
- text-align: left;
-}
-
-.drawer__header {
- flex: 0 0 auto;
- font-size: 16px;
- background: lighten($ui-base-color, 8%);
- margin-bottom: 10px;
- display: flex;
- flex-direction: row;
-
- a {
- transition: background 100ms ease-in;
-
- &:hover {
- background: lighten($ui-base-color, 3%);
- transition: background 200ms ease-out;
- }
- }
-}
-
.tabs-bar {
+ box-sizing: border-box;
display: flex;
background: lighten($ui-base-color, 8%);
flex: 0 0 auto;
@@ -2041,9 +1933,199 @@ a.account__display-name {
}
}
-@media screen and (min-width: 631px) {
+.columns-area--mobile {
+ flex-direction: column;
+ width: 100%;
+ max-width: 600px;
+ margin: 0 auto;
+
+ .column,
+ .drawer {
+ width: 100%;
+ height: 100%;
+ padding: 0;
+ }
+
+ .search__input,
+ .autosuggest-textarea__textarea {
+ font-size: 16px;
+ }
+
+ @media screen and (min-width: 360px) {
+ padding: 10px;
+ }
+
+ @media screen and (min-width: 630px) {
+ .detailed-status {
+ padding: 15px;
+
+ .media-gallery,
+ .video-player {
+ margin-top: 15px;
+ }
+ }
+
+ .account__header__bar {
+ padding: 5px 10px;
+ }
+
+ .navigation-bar,
+ .compose-form {
+ padding: 15px;
+ }
+
+ .compose-form .compose-form__publish .compose-form__publish-button-wrapper {
+ padding-top: 15px;
+ }
+
+ .status {
+ padding: 15px 15px 15px (48px + 15px * 2);
+ min-height: 48px + 2px;
+
+ &__avatar {
+ left: 15px;
+ top: 17px;
+ }
+
+ &__content {
+ padding-top: 5px;
+ }
+
+ &__prepend {
+ margin-left: 48px + 15px * 2;
+ padding-top: 15px;
+ }
+
+ &__prepend-icon-wrapper {
+ left: -32px;
+ }
+
+ .media-gallery,
+ &__action-bar,
+ .video-player {
+ margin-top: 10px;
+ }
+ }
+ }
+}
+
+@media screen and (min-width: 360px) {
.tabs-bar {
- display: none;
+ margin: 10px auto;
+ margin-bottom: 0;
+ width: calc(100% - 20px);
+ max-width: 600px;
+ }
+
+ .react-swipeable-view-container .columns-area--mobile {
+ height: calc(100% - 20px) !important;
+ }
+
+ .getting-started__wrapper,
+ .getting-started__trends,
+ .search {
+ margin-bottom: 10px;
+ }
+}
+
+.icon-with-badge {
+ position: relative;
+
+ &__badge {
+ position: absolute;
+ right: -13px;
+ top: -13px;
+ background: $ui-highlight-color;
+ border: 2px solid lighten($ui-base-color, 8%);
+ padding: 1px 6px;
+ border-radius: 6px;
+ font-size: 10px;
+ font-weight: 500;
+ line-height: 14px;
+ color: $primary-text-color;
+ }
+}
+
+.drawer__pager {
+ box-sizing: border-box;
+ padding: 0;
+ flex-grow: 1;
+ position: relative;
+ overflow: hidden;
+ display: flex;
+}
+
+.drawer__inner {
+ position: absolute;
+ top: 0;
+ left: 0;
+ background: lighten($ui-base-color, 13%);
+ box-sizing: border-box;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ overflow-y: auto;
+ width: 100%;
+ height: 100%;
+
+ &.darker {
+ background: $ui-base-color;
+ }
+}
+
+.drawer__inner__mastodon {
+ background: lighten($ui-base-color, 13%) url('data:image/svg+xml;utf8,') no-repeat bottom / 100% auto;
+ flex: 1;
+ min-height: 47px;
+ display: none;
+
+ > img {
+ display: block;
+ object-fit: contain;
+ object-position: bottom left;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ user-drag: none;
+ user-select: none;
+ }
+
+ @media screen and (min-height: 640px) {
+ display: block;
+ }
+}
+
+.navigational-toggle {
+ padding: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ font-size: 14px;
+ color: $dark-text-color;
+}
+
+.pseudo-drawer {
+ background: lighten($ui-base-color, 13%);
+ font-size: 13px;
+ text-align: left;
+}
+
+.drawer__header {
+ flex: 0 0 auto;
+ font-size: 16px;
+ background: lighten($ui-base-color, 8%);
+ margin-bottom: 10px;
+ display: flex;
+ flex-direction: row;
+
+ a {
+ transition: background 100ms ease-in;
+
+ &:hover {
+ background: lighten($ui-base-color, 3%);
+ transition: background 200ms ease-out;
+ }
}
}
@@ -3190,6 +3272,10 @@ a.status-card.compact:hover {
contain: strict;
}
+ & > span {
+ max-width: 400px;
+ }
+
a {
color: $highlight-text-color;
text-decoration: none;
@@ -5611,3 +5697,49 @@ noscript {
}
}
}
+
+.layout-toggle {
+ display: flex;
+ padding: 5px;
+
+ button {
+ box-sizing: border-box;
+ flex: 0 0 50%;
+ background: transparent;
+ padding: 5px;
+ border: 0;
+ position: relative;
+
+ &:hover,
+ &:focus,
+ &:active {
+ svg path:first-child {
+ fill: lighten($ui-base-color, 16%);
+ }
+ }
+ }
+
+ svg {
+ width: 100%;
+ height: auto;
+
+ path:first-child {
+ fill: lighten($ui-base-color, 12%);
+ }
+
+ path:last-child {
+ fill: darken($ui-base-color, 14%);
+ }
+ }
+
+ &__active {
+ color: $ui-highlight-color;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ background: lighten($ui-base-color, 12%);
+ border-radius: 50%;
+ padding: 0.35rem;
+ }
+}