return (
<div className={wrapperClassName}>
- <h1 tabIndex={focusable && '0'} role='button' className={buttonClassName} aria-label={title} onClick={this.handleTitleClick}>
+ <h1 tabIndex={focusable ? 0 : null} role='button' className={buttonClassName} aria-label={title} onClick={this.handleTitleClick}>
<i className={`fa fa-fw fa-${icon} column-header__icon`} />
{title}
-
<div className='column-header__buttons'>
{backButton}
+ { notifCleaning ? (
+ <button
+ aria-label={msgEnterNotifCleaning}
+ title={msgEnterNotifCleaning}
+ onClick={this.onEnterCleaningMode}
+ className={notifCleaningButtonClassName}
+ >
+ <i className='fa fa-eraser' />
+ </button>
+ ) : null}
{collapseButton}
</div>
</h1>
- <div className={collapsibleClassName} tabIndex={collapsed && -1} onTransitionEnd={this.handleTransitionEnd}>
+ { notifCleaning ? (
+ <div className={notifCleaningDrawerClassName} onTransitionEnd={this.handleTransitionEndNCD}>
+ <div className='column-header__collapsible-inner nopad-drawer'>
+ {(notifCleaningActive || animatingNCD) ? (<NotificationPurgeButtonsContainer />) : null }
+ </div>
+ </div>
+ ) : null}
+
+ <div className={collapsibleClassName} tabIndex={collapsed ? -1 : null} onTransitionEnd={this.handleTransitionEnd}>
<div className='column-header__collapsible-inner'>
{(!collapsed || animating) && collapsedContent}
</div>
import ImmutablePropTypes from 'react-immutable-proptypes';
import { FormattedMessage } from 'react-intl';
import AccountContainer from '../../../containers/account_container';
-import StatusContainer from '../../../containers/status_container';
+import StatusContainer from '../../../../glitch/components/status/container';
- import Link from 'react-router-dom/Link';
+ import { Link } from 'react-router-dom';
import ImmutablePureComponent from 'react-immutable-pure-component';
export default class SearchResults extends ImmutablePureComponent {
import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux';
import { mountCompose, unmountCompose } from '../../actions/compose';
- import Link from 'react-router-dom/Link';
+import { openModal } from '../../actions/modal';
+import { changeLocalSetting } from '../../../glitch/actions/local_settings';
+ import { Link } from 'react-router-dom';
import { injectIntl, defineMessages } from 'react-intl';
import SearchContainer from './containers/search_container';
import Motion from 'react-motion/lib/Motion';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Avatar from '../../../components/avatar';
import DisplayName from '../../../components/display_name';
-import StatusContent from '../../../components/status_content';
-import MediaGallery from '../../../components/media_gallery';
+import StatusContent from '../../../../glitch/components/status/content';
+import StatusGallery from '../../../../glitch/components/status/gallery';
+import StatusPlayer from '../../../../glitch/components/status/player';
import AttachmentList from '../../../components/attachment_list';
- import Link from 'react-router-dom/Link';
+ import { Link } from 'react-router-dom';
import { FormattedDate, FormattedNumber } from 'react-intl';
import CardContainer from '../containers/card_container';
import ImmutablePureComponent from 'react-immutable-pure-component';
<ColumnBackButton />
<ScrollContainer scrollKey='thread'>
- <div className='scrollable detailed-status__wrapper'>
+ <div className='scrollable detailed-status__wrapper' ref={this.setRef}>
{ancestors}
- <DetailedStatus
- status={status}
- settings={settings}
- autoPlayGif={autoPlayGif}
- me={me}
- onOpenVideo={this.handleOpenVideo}
- onOpenMedia={this.handleOpenMedia}
- />
-
- <ActionBar
- status={status}
- me={me}
- onReply={this.handleReplyClick}
- onFavourite={this.handleFavouriteClick}
- onReblog={this.handleReblogClick}
- onDelete={this.handleDeleteClick}
- onMention={this.handleMentionClick}
- onReport={this.handleReport}
- onPin={this.handlePin}
- onEmbed={this.handleEmbed}
- />
+ <HotKeys handlers={handlers}>
+ <div className='focusable' tabIndex='0'>
+ <DetailedStatus
+ status={status}
++ settings={settings}
+ autoPlayGif={autoPlayGif}
+ me={me}
+ onOpenVideo={this.handleOpenVideo}
+ onOpenMedia={this.handleOpenMedia}
+ />
+
+ <ActionBar
+ status={status}
+ me={me}
+ onReply={this.handleReplyClick}
+ onFavourite={this.handleFavouriteClick}
+ onReblog={this.handleReblogClick}
+ onDelete={this.handleDeleteClick}
+ onMention={this.handleMentionClick}
+ onReport={this.handleReport}
+ onPin={this.handlePin}
+ onEmbed={this.handleEmbed}
+ />
+ </div>
+ </HotKeys>
{descendants}
</div>
import React from 'react';
import PropTypes from 'prop-types';
- import Link from 'react-router-dom/Link';
+ import { Link } from 'react-router-dom';
-const ColumnLink = ({ icon, text, to, href, method }) => {
+const ColumnLink = ({ icon, text, to, onClick, href, method }) => {
if (href) {
return (
<a href={href} className='column-link' data-method={method}>
// Dummy import, to make sure that <Status /> ends up in the application bundle.
// Without this it ends up in ~8 very commonly used bundles.
-import '../../components/status';
+import '../../../glitch/components/status';
const mapStateToProps = state => ({
+ systemFontUi: state.getIn(['meta', 'system_font_ui']),
+ layout: state.getIn(['local_settings', 'layout']),
+ isWide: state.getIn(['local_settings', 'stretch']),
+ navbarUnder: state.getIn(['local_settings', 'navbar_under']),
+ me: state.getIn(['meta', 'me']),
isComposing: state.getIn(['compose', 'is_composing']),
});
static propTypes = {
dispatch: PropTypes.func.isRequired,
children: PropTypes.node,
+ layout: PropTypes.string,
+ isWide: PropTypes.bool,
+ systemFontUi: PropTypes.bool,
+ navbarUnder: PropTypes.bool,
isComposing: PropTypes.bool,
+ me: PropTypes.string,
location: PropTypes.object,
};
render () {
const { width, draggingOver } = this.state;
- const { children } = this.props;
+ const { children, layout, isWide, navbarUnder } = this.props;
+
+ const columnsClass = layout => {
+ switch (layout) {
+ case 'single':
+ return 'single-column';
+ case 'multiple':
+ return 'multi-columns';
+ default:
+ return 'auto-columns';
+ }
+ };
+
+ const className = classNames('ui', columnsClass(layout), {
+ 'wide': isWide,
+ 'system-font': this.props.systemFontUi,
+ 'navbar-under': navbarUnder,
+ });
+ const handlers = {
+ new: this.handleHotkeyNew,
+ search: this.handleHotkeySearch,
+ forceNew: this.handleHotkeyForceNew,
+ focusColumn: this.handleHotkeyFocusColumn,
+ back: this.handleHotkeyBack,
+ goToHome: this.handleHotkeyGoToHome,
+ goToNotifications: this.handleHotkeyGoToNotifications,
+ goToLocal: this.handleHotkeyGoToLocal,
+ goToFederated: this.handleHotkeyGoToFederated,
+ goToStart: this.handleHotkeyGoToStart,
+ goToFavourites: this.handleHotkeyGoToFavourites,
+ goToPinned: this.handleHotkeyGoToPinned,
+ goToProfile: this.handleHotkeyGoToProfile,
+ goToBlocked: this.handleHotkeyGoToBlocked,
+ goToMuted: this.handleHotkeyGoToMuted,
+ };
+
return (
- <div className={className} ref={this.setRef}>
- {navbarUnder ? null : (<TabsBar />)}
- <ColumnsAreaContainer ref={this.setColumnsAreaRef} singleColumn={isMobile(width, layout)}>
- <WrappedSwitch>
- <Redirect from='/' to='/getting-started' exact />
- <WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
- <WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} />
- <WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} />
- <WrappedRoute path='/timelines/public/local' component={CommunityTimeline} content={children} />
- <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
-
- <WrappedRoute path='/notifications' component={Notifications} content={children} />
- <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} />
- <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
-
- <WrappedRoute path='/statuses/new' component={Compose} content={children} />
- <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />
- <WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} />
- <WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} />
-
- <WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} />
- <WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} />
- <WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} />
- <WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} />
-
- <WrappedRoute path='/follow_requests' component={FollowRequests} content={children} />
- <WrappedRoute path='/blocks' component={Blocks} content={children} />
- <WrappedRoute path='/mutes' component={Mutes} content={children} />
-
- <WrappedRoute component={GenericNotFound} content={children} />
- </WrappedSwitch>
- </ColumnsAreaContainer>
- <NotificationsContainer />
- {navbarUnder ? (<TabsBar />) : null}
- <LoadingBarContainer className='loading-bar' />
- <ModalContainer />
- <UploadArea active={draggingOver} onClose={this.closeUploadModal} />
- </div>
+ <HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef}>
- <div className='ui' ref={this.setRef}>
- <TabsBar />
++ <div className={className} ref={this.setRef}>
++ {navbarUnder ? null : (<TabsBar />)}
+
- <ColumnsAreaContainer ref={this.setColumnsAreaRef} singleColumn={isMobile(width)}>
++ <ColumnsAreaContainer ref={this.setColumnsAreaRef} singleColumn={isMobile(width, layout)}>
+ <WrappedSwitch>
+ <Redirect from='/' to='/getting-started' exact />
+ <WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
+ <WrappedRoute path='/timelines/home' component={HomeTimeline} content={children} />
+ <WrappedRoute path='/timelines/public' exact component={PublicTimeline} content={children} />
+ <WrappedRoute path='/timelines/public/local' component={CommunityTimeline} content={children} />
+ <WrappedRoute path='/timelines/tag/:id' component={HashtagTimeline} content={children} />
+
+ <WrappedRoute path='/notifications' component={Notifications} content={children} />
+ <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} />
+ <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
+
+ <WrappedRoute path='/statuses/new' component={Compose} content={children} />
+ <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />
+ <WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} />
+ <WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} />
+
+ <WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} />
+ <WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} />
+ <WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} />
+ <WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} />
+
+ <WrappedRoute path='/follow_requests' component={FollowRequests} content={children} />
+ <WrappedRoute path='/blocks' component={Blocks} content={children} />
+ <WrappedRoute path='/mutes' component={Mutes} content={children} />
+
+ <WrappedRoute component={GenericNotFound} content={children} />
+ </WrappedSwitch>
+ </ColumnsAreaContainer>
+
+ <NotificationsContainer />
++ {navbarUnder ? (<TabsBar />) : null}
+ <LoadingBarContainer className='loading-bar' />
+ <ModalContainer />
+ <UploadArea active={draggingOver} onClose={this.closeUploadModal} />
+ </div>
+ </HotKeys>
);
}
NOTIFICATIONS_EXPAND_FAIL,
NOTIFICATIONS_CLEAR,
NOTIFICATIONS_SCROLL_TOP,
+ NOTIFICATIONS_DELETE_MARKED_REQUEST,
+ NOTIFICATIONS_DELETE_MARKED_SUCCESS,
+ NOTIFICATION_MARK_FOR_DELETE,
+ NOTIFICATIONS_DELETE_MARKED_FAIL,
+ NOTIFICATIONS_ENTER_CLEARING_MODE,
+ NOTIFICATIONS_MARK_ALL_FOR_DELETE,
} from '../actions/notifications';
- import { ACCOUNT_BLOCK_SUCCESS } from '../actions/accounts';
+ import {
+ ACCOUNT_BLOCK_SUCCESS,
+ ACCOUNT_MUTE_SUCCESS,
+ } from '../actions/accounts';
import { TIMELINE_DELETE } from '../actions/timelines';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import uuid from '../uuid';
const initialState = ImmutableMap({
+ saved: true,
+
onboarded: false,
+ layout: 'auto',
+ skinTone: 1,
+
home: ImmutableMap({
shows: ImmutableMap({
reblog: true,
}
.status__prepend-icon-wrapper {
- left: -26px;
- position: absolute;
+ float: left;
+ margin: 0 10px 0 -58px;
+ width: 48px;
+ text-align: right;
+}
+
+.notif-cleaning {
+ .status, .notification-follow {
+ padding-right: ($dismiss-overlay-width + 0.5rem);
+ }
+}
+
+.notification-follow {
+ position: relative;
+
+ // same like Status
+ border-bottom: 1px solid lighten($ui-base-color, 8%);
+
+ .account {
+ border-bottom: 0 none;
+ }
}
+ .focusable {
+ &:focus {
+ outline: 0;
+ background: lighten($ui-base-color, 4%);
+
+ &.status-direct {
+ background: lighten($ui-base-color, 12%);
+ }
+
+ .detailed-status,
+ .detailed-status__action-bar {
+ background: lighten($ui-base-color, 8%);
+ }
+ }
+ }
+
.status {
padding: 8px 10px;
- padding-left: 68px;
position: relative;
+ height: auto;
min-height: 48px;
border-bottom: 1px solid lighten($ui-base-color, 8%);
cursor: default;
}
}
-.status__display-name,
-.reply-indicator__display-name,
-.detailed-status__display-name,
-.account__display-name {
- &:hover strong {
- text-decoration: underline;
- }
-}
-
+ .muted {
+ .emojione {
+ opacity: 0.5;
+ }
+ }
+
.account__display-name strong {
display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
.detailed-status__application,
end
end
+ def prepare_description
+ self.description = description.strip[0...420] unless description.nil?
+ end
+
def set_type_and_extension
- self.type = VIDEO_MIME_TYPES.include?(file_content_type) ? :video : :image
- extension = appropriate_extension
+ self.type = VIDEO_MIME_TYPES.include?(file_content_type) ? :video : AUDIO_MIME_TYPES.include?(file_content_type) ? :audio : :image
+ extension = AUDIO_MIME_TYPES.include?(file_content_type) ? '.mp4' : appropriate_extension
basename = Paperclip::Interpolations.basename(file, :original)
file.instance_write :file_name, [basename, extension].delete_if(&:blank?).join('.')
end
#
# Table name: mutes
#
- # id :integer not null, primary key
- # account_id :integer not null
- # target_account_id :integer not null
-# created_at :datetime not null
-# updated_at :datetime not null
-# account_id :integer not null
-# id :integer not null, primary key
-# target_account_id :integer not null
+# created_at :datetime not null
+# updated_at :datetime not null
++# account_id :integer not null
++# id :integer not null, primary key
++# target_account_id :integer not null
+# hide_notifications :boolean default(TRUE), not null
#
class Mute < ApplicationRecord
# frozen_string_literal: true
class MuteService < BaseService
- def call(account, target_account)
+ def call(account, target_account, notifications: nil)
return if account.id == target_account.id
- FeedManager.instance.clear_from_timeline(account, target_account)
- mute = account.mute!(target_account)
+ account.mute!(target_account, notifications: notifications)
+ BlockWorker.perform_async(account.id, target_account.id)
+ mute
end
end
%head
%meta{ content: 'text/html; charset=UTF-8', 'http-equiv' => 'Content-Type' }/
%meta{ charset: 'utf-8' }/
- %title= safe_join([yield(:page_title), title], ' - ')
+ %title= safe_join([yield(:page_title), Setting.default_settings['site_title']], ' - ')
%meta{ content: 'width=device-width,initial-scale=1', name: 'viewport' }/
= stylesheet_pack_tag 'common', media: 'all'
- = stylesheet_pack_tag Setting.default_settings['theme'], media: 'all'
+ = stylesheet_pack_tag 'application', integrity: true, media: 'all'
%body.error
.dialog
- %img{ alt: title, src: '/oops.gif' }/
+ %img{ alt: Setting.default_settings['site_title'], src: '/oops.gif' }/
%div
%h1= yield :content
require_relative '../app/lib/exceptions'
require_relative '../lib/paperclip/gif_transcoder'
require_relative '../lib/paperclip/video_transcoder'
+require_relative '../lib/paperclip/audio_transcoder'
+ require_relative '../lib/mastodon/snowflake'
require_relative '../lib/mastodon/version'
Dotenv::Railtie.load
config.action_mailer.delivery_method = ENV.fetch('SMTP_DELIVERY_METHOD', 'smtp').to_sym
- config.to_prepare do
- StatsD.backend = StatsD::Instrument::Backends::NullBackend.new if ENV['STATSD_ADDR'].blank?
- Sidekiq::Logging.logger.level = Logger::WARN
- end
-
config.action_dispatch.default_headers = {
- 'Server' => 'Mastodon',
- 'X-Frame-Options' => 'DENY',
- 'X-Content-Type-Options' => 'nosniff',
- 'X-XSS-Protection' => '1; mode=block',
+ 'Server' => 'Mastodon',
+ 'X-Frame-Options' => 'DENY',
+ 'X-Content-Type-Options' => 'nosniff',
+ 'X-XSS-Protection' => '1; mode=block',
+ 'Content-Security-Policy' => "frame-ancestors 'none'; object-src 'none'; script-src 'self' https://dev-static.glitch.social 'unsafe-inline'; base-uri 'none';" ,
+ 'Referrer-Policy' => 'no-referrer, strict-origin-when-cross-origin',
+ 'Strict-Transport-Security' => 'max-age=63072000; includeSubDomains; preload'
}
end
get '/search', to: 'search#index', as: :search
resources :follows, only: [:create]
- resources :media, only: [:create]
- resources :apps, only: [:create]
+ resources :media, only: [:create, :update]
resources :blocks, only: [:index]
- resources :mutes, only: [:index]
+ resources :mutes, only: [:index] do
+ collection do
+ get 'details'
+ end
+ end
resources :favourites, only: [:index]
resources :reports, only: [:index, :create]
module.exports = {
test: /\.js$/,
- // include react-intl because transform-react-remove-prop-types needs to apply to it
- exclude: {
- test: /node_modules/,
- exclude: /react-intl[\/\\](?!locale-data)/,
- },
+ exclude: /node_modules/,
loader: 'babel-loader',
options: {
- forceEnv: env,
+ forceEnv: process.env.NODE_ENV || 'development',
+ sourceRoot: 'app/javascript',
cacheDirectory: env === 'development' ? false : resolve(__dirname, '..', '..', '..', 'tmp', 'cache', 'babel-loader'),
},
};
plugins: [
new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(env))),
- new ExtractTextPlugin(env.NODE_ENV === 'production' ? '[name]-[hash].css' : '[name].css'),
+ new webpack.NormalModuleReplacementPlugin(
+ /^history\//, (resource) => {
+ // temporary fix for https://github.com/ReactTraining/react-router/issues/5576
+ // to reduce bundle size
+ resource.request = resource.request.replace(/^history/, 'history/es');
+ }
+ ),
+ new ExtractTextPlugin({
+ filename: env.NODE_ENV === 'production' ? '[name]-[hash].css' : '[name].css',
+ allChunks: true,
+ }),
new ManifestPlugin({
publicPath: output.publicPath,
writeToFileEmit: true,
end
create_table "mutes", force: :cascade do |t|
- t.bigint "account_id", null: false
- t.bigint "target_account_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.boolean "hide_notifications", default: true, null: false
+ t.bigint "account_id", null: false
+ t.bigint "target_account_id", null: false
t.index ["account_id", "target_account_id"], name: "index_mutes_on_account_id_and_target_account_id", unique: true
end