Browse Source

Merge branch 'master' into live

master
Zac 1 week ago
parent
commit
b1316776b9
59 changed files with 893 additions and 548 deletions
  1. 3
    0
      .env.nanobox
  2. 3
    0
      .env.production.sample
  3. 2
    2
      Dockerfile
  4. 5
    5
      Gemfile
  5. 12
    12
      Gemfile.lock
  6. 2
    1
      app/controllers/admin/reports_controller.rb
  7. 2
    0
      app/controllers/api/proofs_controller.rb
  8. 1
    1
      app/helpers/admin/filter_helper.rb
  9. 1
    1
      app/helpers/domain_control_helper.rb
  10. 2
    3
      app/javascript/flavours/glitch/actions/compose.js
  11. 11
    4
      app/javascript/flavours/glitch/components/modal_root.js
  12. 17
    0
      app/javascript/flavours/glitch/components/status.js
  13. 3
    2
      app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js
  14. 4
    0
      app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js
  15. 17
    0
      app/javascript/flavours/glitch/features/status/index.js
  16. 14
    2
      app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js
  17. 17
    2
      app/javascript/flavours/glitch/features/ui/index.js
  18. 1
    1
      app/javascript/flavours/glitch/features/video/index.js
  19. 1
    1
      app/javascript/flavours/glitch/reducers/compose.js
  20. 34
    0
      app/javascript/flavours/glitch/styles/components/index.scss
  21. 1
    1
      app/javascript/flavours/glitch/styles/containers.scss
  22. 7
    1
      app/javascript/flavours/glitch/styles/forms.scss
  23. 2
    3
      app/javascript/mastodon/actions/compose.js
  24. 10
    4
      app/javascript/mastodon/components/modal_root.js
  25. 17
    0
      app/javascript/mastodon/components/status.js
  26. 1
    1
      app/javascript/mastodon/features/compose/components/poll_form.js
  27. 3
    2
      app/javascript/mastodon/features/direct_timeline/components/conversation.js
  28. 4
    0
      app/javascript/mastodon/features/keyboard_shortcuts/index.js
  29. 17
    0
      app/javascript/mastodon/features/status/index.js
  30. 14
    2
      app/javascript/mastodon/features/ui/components/focal_point_modal.js
  31. 1
    0
      app/javascript/mastodon/features/ui/index.js
  32. 1
    1
      app/javascript/mastodon/features/video/index.js
  33. 1
    1
      app/javascript/mastodon/reducers/compose.js
  34. 26
    0
      app/javascript/styles/mastodon/components.scss
  35. 1
    1
      app/javascript/styles/mastodon/containers.scss
  36. 7
    1
      app/javascript/styles/mastodon/forms.scss
  37. 14
    6
      app/lib/activitypub/activity/create.rb
  38. 2
    2
      app/models/account.rb
  39. 10
    2
      app/models/concerns/ldap_authenticable.rb
  40. 2
    0
      app/models/report_filter.rb
  41. 1
    1
      app/models/user.rb
  42. 2
    1
      app/presenters/status_relationships_presenter.rb
  43. 2
    2
      app/serializers/rest/status_serializer.rb
  44. 1
    1
      app/services/activitypub/process_poll_service.rb
  45. 2
    0
      app/services/block_domain_service.rb
  46. 1
    1
      app/services/fetch_link_card_service.rb
  47. 1
    1
      app/services/fetch_oembed_service.rb
  48. 7
    7
      app/views/about/_registration.html.haml
  49. 14
    0
      app/views/admin/reports/index.html.haml
  50. 2
    2
      app/views/relationships/show.html.haml
  51. 6
    0
      app/views/settings/preferences/notifications/show.html.haml
  52. 9
    0
      config/initializers/devise.rb
  53. 14
    2
      config/initializers/doorkeeper.rb
  54. 7
    0
      config/locales/en.yml
  55. 8
    8
      config/locales/simple_form.en.yml
  56. 41
    0
      config/pghero.yml
  57. 7
    7
      package.json
  58. 1
    1
      spec/services/fetch_link_card_service_spec.rb
  59. 474
    449
      yarn.lock

+ 3
- 0
.env.nanobox View File

@@ -183,6 +183,9 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io
# LDAP_BIND_DN=
# LDAP_PASSWORD=
# LDAP_UID=cn
# LDAP_UID_CONVERSION_ENABLED=true
# LDAP_UID_CONVERSION_SEARCH=., -
# LDAP_UID_CONVERSION_REPLACE=_

# PAM authentication (optional)
# PAM authentication uses for the email generation the "email" pam variable

+ 3
- 0
.env.production.sample View File

@@ -208,6 +208,9 @@ STREAMING_CLUSTER_NUM=1
# LDAP_PASSWORD=
# LDAP_UID=cn
# LDAP_SEARCH_FILTER=%{uid}=%{email}
# LDAP_UID_CONVERSION_ENABLED=true
# LDAP_UID_CONVERSION_SEARCH=., -
# LDAP_UID_CONVERSION_REPLACE=_

# PAM authentication (optional)
# PAM authentication uses for the email generation the "email" pam variable

+ 2
- 2
Dockerfile View File

@@ -3,8 +3,8 @@ FROM ubuntu:18.04 as build-dep
# Use bash for the shell
SHELL ["bash", "-c"]

# Install Node
ENV NODE_VER="12.11.1"
# Install Node v12 (LTS)
ENV NODE_VER="12.13.1"
RUN echo "Etc/UTC" > /etc/localtime && \
apt update && \
apt -y install wget python && \

+ 5
- 5
Gemfile View File

@@ -12,7 +12,7 @@ gem 'thor', '~> 0.20'
gem 'hamlit-rails', '~> 0.2'
gem 'pg', '~> 1.1'
gem 'makara', '~> 0.4'
gem 'pghero', '~> 2.3'
gem 'pghero', '~> 2.4'
gem 'dotenv-rails', '~> 2.7'

gem 'aws-sdk-s3', '~> 1.55', require: false
@@ -27,7 +27,7 @@ gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.7'
gem 'bootsnap', '~> 1.4', require: false
gem 'browser'
gem 'charlock_holmes', '~> 0.7.6'
gem 'charlock_holmes', '~> 0.7.7'
gem 'iso-639'
gem 'chewy', '~> 5.1'
gem 'cld3', '~> 3.2.4'
@@ -38,7 +38,7 @@ group :pam_authentication, optional: true do
gem 'devise_pam_authenticatable2', '~> 9.2'
end

gem 'net-ldap', '~> 0.10'
gem 'net-ldap', '~> 0.16'
gem 'omniauth-cas', '~> 1.1'
gem 'omniauth-saml', '~> 1.10'
gem 'omniauth', '~> 1.9'
@@ -68,12 +68,12 @@ gem 'oj', '~> 3.9'
gem 'ostatus2', '~> 2.0'
gem 'ox', '~> 2.11'
gem 'parslet'
gem 'parallel', '~> 1.18'
gem 'parallel', '~> 1.19'
gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c'
gem 'pundit', '~> 2.1'
gem 'premailer-rails'
gem 'rack-attack', '~> 6.2'
gem 'rack-cors', '~> 1.0', require: 'rack/cors'
gem 'rack-cors', '~> 1.1', require: 'rack/cors'
gem 'rails-i18n', '~> 5.1'
gem 'rails-settings-cached', '~> 0.6'
gem 'redis', '~> 4.1', require: ['redis', 'redis/connection/hiredis']

+ 12
- 12
Gemfile.lock View File

@@ -133,7 +133,7 @@ GEM
bootsnap (1.4.5)
msgpack (~> 1.0)
brakeman (4.7.1)
browser (2.6.1)
browser (2.7.1)
builder (3.2.3)
bullet (6.0.2)
activesupport (>= 3.0.0)
@@ -168,7 +168,7 @@ GEM
xpath (~> 3.2)
case_transform (0.2)
activesupport
charlock_holmes (0.7.6)
charlock_holmes (0.7.7)
chewy (5.1.0)
activesupport (>= 4.0)
elasticsearch (>= 2.0.0)
@@ -385,7 +385,7 @@ GEM
multi_json (1.13.1)
multipart-post (2.1.1)
necromancer (0.5.0)
net-ldap (0.16.1)
net-ldap (0.16.2)
net-scp (2.0.0)
net-ssh (>= 2.6.5, < 6.0.0)
net-ssh (5.2.0)
@@ -425,7 +425,7 @@ GEM
paperclip-av-transcoder (0.6.4)
av (~> 0.9.0)
paperclip (>= 2.5.2)
parallel (1.18.0)
parallel (1.19.1)
parallel_tests (2.29.2)
parallel
parser (2.6.5.0)
@@ -435,7 +435,7 @@ GEM
equatable (~> 0.6)
tty-color (~> 0.5)
pg (1.1.4)
pghero (2.3.0)
pghero (2.4.1)
activerecord (>= 5)
pkg-config (1.4.0)
premailer (1.11.1)
@@ -463,8 +463,8 @@ GEM
rack (2.0.7)
rack-attack (6.2.1)
rack (>= 1.0, < 3)
rack-cors (1.0.6)
rack (>= 1.6.0)
rack-cors (1.1.0)
rack (>= 2.0.0)
rack-protection (2.0.7)
rack
rack-proxy (0.6.5)
@@ -699,7 +699,7 @@ DEPENDENCIES
capistrano-rbenv (~> 2.1)
capistrano-yarn (~> 2.0)
capybara (~> 3.29)
charlock_holmes (~> 0.7.6)
charlock_holmes (~> 0.7.7)
chewy (~> 5.1)
cld3 (~> 3.2.4)
climate_control (~> 0.2)
@@ -744,7 +744,7 @@ DEPENDENCIES
memory_profiler
microformats (~> 4.1)
mime-types (~> 3.3)
net-ldap (~> 0.10)
net-ldap (~> 0.16)
nilsimsa!
nokogiri (~> 1.10)
nsa (~> 0.2)
@@ -756,11 +756,11 @@ DEPENDENCIES
ox (~> 2.11)
paperclip (~> 6.0)
paperclip-av-transcoder (~> 0.6)
parallel (~> 1.18)
parallel (~> 1.19)
parallel_tests (~> 2.29)
parslet
pg (~> 1.1)
pghero (~> 2.3)
pghero (~> 2.4)
pkg-config (~> 1.4)
posix-spawn!
premailer-rails
@@ -770,7 +770,7 @@ DEPENDENCIES
puma (~> 4.2)
pundit (~> 2.1)
rack-attack (~> 6.2)
rack-cors (~> 1.0)
rack-cors (~> 1.1)
rails (~> 5.2.3)
rails-controller-testing (~> 1.0)
rails-i18n (~> 5.1)

+ 2
- 1
app/controllers/admin/reports_controller.rb View File

@@ -55,7 +55,8 @@ module Admin
params.permit(
:account_id,
:resolved,
:target_account_id
:target_account_id,
:by_target_domain
)
end


+ 2
- 0
app/controllers/api/proofs_controller.rb View File

@@ -3,6 +3,8 @@
class Api::ProofsController < Api::BaseController
include AccountOwnedConcern

skip_before_action :require_authenticated_user!

before_action :set_provider

def index

+ 1
- 1
app/helpers/admin/filter_helper.rb View File

@@ -2,7 +2,7 @@

module Admin::FilterHelper
ACCOUNT_FILTERS = %i(local remote by_domain active pending silenced suspended username display_name email ip staff).freeze
REPORT_FILTERS = %i(resolved account_id target_account_id).freeze
REPORT_FILTERS = %i(resolved account_id target_account_id by_target_domain).freeze
INVITE_FILTER = %i(available expired).freeze
CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze
TAGS_FILTERS = %i(directory reviewed unreviewed pending_review popular active name).freeze

+ 1
- 1
app/helpers/domain_control_helper.rb View File

@@ -6,7 +6,7 @@ module DomainControlHelper

domain = begin
if uri_or_domain.include?('://')
Addressable::URI.parse(uri_or_domain).domain
Addressable::URI.parse(uri_or_domain).host
else
uri_or_domain
end

+ 2
- 3
app/javascript/flavours/glitch/actions/compose.js View File

@@ -263,7 +263,7 @@ export function uploadCompose(files) {
dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
},
}).then(({ data }) => dispatch(uploadComposeSuccess(data, f)));
}).catch(error => dispatch(uploadComposeFail(error, true)));
}).catch(error => dispatch(uploadComposeFail(error)));
};
};
};
@@ -294,11 +294,10 @@ export function changeUploadComposeSuccess(media) {
};
};

export function changeUploadComposeFail(error, decrement = false) {
export function changeUploadComposeFail(error) {
return {
type: COMPOSE_UPLOAD_CHANGE_FAIL,
error: error,
decrement: decrement,
skipLoading: true,
};
};

+ 11
- 4
app/javascript/flavours/glitch/components/modal_root.js View File

@@ -62,15 +62,22 @@ export default class ModalRoot extends React.PureComponent {
} else if (!nextProps.children) {
this.setState({ revealed: false });
}
if (!nextProps.children && !!this.props.children) {
this.activeElement.focus({ preventScroll: true });
this.activeElement = null;
}
}

componentDidUpdate (prevProps) {
if (!this.props.children && !!prevProps.children) {
this.getSiblings().forEach(sibling => sibling.removeAttribute('inert'));

// Because of the wicg-inert polyfill, the activeElement may not be
// immediately selectable, we have to wait for observers to run, as
// described in https://github.com/WICG/inert#performance-and-gotchas
Promise.resolve().then(() => {
this.activeElement.focus({ preventScroll: true });
this.activeElement = null;
}).catch((error) => {
console.error(error);
});

this.handleModalClose();
}
if (this.props.children) {

+ 17
- 0
app/javascript/flavours/glitch/components/status.js View File

@@ -376,6 +376,22 @@ class Status extends ImmutablePureComponent {
this.props.onOpenVideo(media, startTime);
}

handleHotkeyOpenMedia = e => {
const { status, onOpenMedia, onOpenVideo } = this.props;

e.preventDefault();

if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
// TODO: toggle play/paused?
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
onOpenVideo(status.getIn(['media_attachments', 0]), 0);
} else {
onOpenMedia(status.get('media_attachments'), 0);
}
}
}

handleHotkeyReply = e => {
e.preventDefault();
this.props.onReply(this.props.status, this.context.router.history);
@@ -503,6 +519,7 @@ class Status extends ImmutablePureComponent {
bookmark: this.handleHotkeyBookmark,
toggleCollapse: this.handleHotkeyCollapse,
toggleSensitive: this.handleHotkeyToggleSensitive,
openMedia: this.handleHotkeyOpenMedia,
};

if (hidden) {

+ 3
- 2
app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js View File

@@ -12,6 +12,7 @@ import IconButton from 'flavours/glitch/components/icon_button';
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
import { HotKeys } from 'react-hotkeys';
import { autoPlayGif } from 'flavours/glitch/util/initial_state';
import classNames from 'classnames';

const messages = defineMessages({
more: { id: 'status.more', defaultMessage: 'More' },
@@ -193,7 +194,7 @@ class Conversation extends ImmutablePureComponent {

return (
<HotKeys handlers={handlers}>
<div className='conversation focusable muted' tabIndex='0'>
<div className={classNames('conversation focusable muted', { 'conversation--unread': unread })} tabIndex='0'>
<div className='conversation__avatar'>
<AvatarComposite accounts={accounts} size={48} />
</div>
@@ -201,7 +202,7 @@ class Conversation extends ImmutablePureComponent {
<div className='conversation__content'>
<div className='conversation__content__info'>
<div className='conversation__content__relative-time'>
<RelativeTimestamp timestamp={lastStatus.get('created_at')} />
{unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} />
</div>

<div className='conversation__content__names' ref={this.setNamesRef}>

+ 4
- 0
app/javascript/flavours/glitch/features/keyboard_shortcuts/index.js View File

@@ -68,6 +68,10 @@ class KeyboardShortcuts extends ImmutablePureComponent {
<td><FormattedMessage id='keyboard_shortcuts.enter' defaultMessage='to open status' /></td>
</tr>
<tr>
<td><kbd>e</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.open_media' defaultMessage='to open media' /></td>
</tr>
<tr>
<td><kbd>x</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.toggle_hidden' defaultMessage='to show/hide text behind CW' /></td>
</tr>

+ 17
- 0
app/javascript/flavours/glitch/features/status/index.js View File

@@ -320,6 +320,22 @@ class Status extends ImmutablePureComponent {
this.props.dispatch(openModal('VIDEO', { media, time }));
}

handleHotkeyOpenMedia = e => {
const { status } = this.props;

e.preventDefault();

if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
// TODO: toggle play/paused?
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
this.handleOpenVideo(status.getIn(['media_attachments', 0]), 0);
} else {
this.handleOpenMedia(status.get('media_attachments'), 0);
}
}
}

handleMuteClick = (account) => {
this.props.dispatch(initMuteModal(account));
}
@@ -529,6 +545,7 @@ class Status extends ImmutablePureComponent {
openProfile: this.handleHotkeyOpenProfile,
toggleSpoiler: this.handleExpandedToggle,
toggleSensitive: this.handleHotkeyToggleSensitive,
openMedia: this.handleHotkeyOpenMedia,
};

return (

+ 14
- 2
app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js View File

@@ -214,7 +214,7 @@ class FocalPointModal extends ImmutablePureComponent {
langPath: `${assetHost}/ocr/lang-data`,
});

let media_url = media.get('file');
let media_url = media.get('url');

if (window.URL && URL.createObjectURL) {
try {
@@ -244,6 +244,16 @@ class FocalPointModal extends ImmutablePureComponent {
const previewWidth = 200;
const previewHeight = previewWidth / previewRatio;

let descriptionLabel = null;

if (media.get('type') === 'audio') {
descriptionLabel = <FormattedMessage id='upload_form.audio_description' defaultMessage='Describe for people with hearing loss' />;
} else if (media.get('type') === 'video') {
descriptionLabel = <FormattedMessage id='upload_form.video_description' defaultMessage='Describe for people with hearing loss or visual impairment' />;
} else {
descriptionLabel = <FormattedMessage id='upload_form.description' defaultMessage='Describe for the visually impaired' />;
}

return (
<div className='modal-root__modal report-modal' style={{ maxWidth: 960 }}>
<div className='report-modal__target'>
@@ -255,7 +265,9 @@ class FocalPointModal extends ImmutablePureComponent {
<div className='report-modal__comment'>
{focals && <p><FormattedMessage id='upload_modal.hint' defaultMessage='Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.' /></p>}

<label className='setting-text-label' htmlFor='upload-modal__description'><FormattedMessage id='upload_form.description' defaultMessage='Describe for the visually impaired' /></label>
<label className='setting-text-label' htmlFor='upload-modal__description'>
{descriptionLabel}
</label>

<div className='setting-text__wrapper'>
<Textarea

+ 17
- 2
app/javascript/flavours/glitch/features/ui/index.js View File

@@ -15,6 +15,7 @@ import { clearHeight } from 'flavours/glitch/actions/height_cache';
import { submitMarkers } from 'flavours/glitch/actions/markers';
import { WrappedSwitch, WrappedRoute } from 'flavours/glitch/util/react_router_helpers';
import UploadArea from './components/upload_area';
import PermaLink from 'flavours/glitch/components/permalink';
import ColumnsAreaContainer from './containers/columns_area_container';
import classNames from 'classnames';
import Favico from 'favico.js';
@@ -51,7 +52,7 @@ import {
} from 'flavours/glitch/util/async-components';
import { HotKeys } from 'react-hotkeys';
import { me } from 'flavours/glitch/util/initial_state';
import { defineMessages, injectIntl } from 'react-intl';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';

// Dummy import, to make sure that <Status /> ends up in the application bundle.
// Without this it ends up in ~8 very commonly used bundles.
@@ -72,6 +73,7 @@ const mapStateToProps = state => ({
unreadNotifications: state.getIn(['notifications', 'unread']),
showFaviconBadge: state.getIn(['local_settings', 'notifications', 'favicon_badge']),
hicolorPrivacyIcons: state.getIn(['local_settings', 'hicolor_privacy_icons']),
moved: state.getIn(['accounts', me, 'moved']) && state.getIn(['accounts', state.getIn(['accounts', me, 'moved'])]),
});

const keyMap = {
@@ -105,6 +107,7 @@ const keyMap = {
bookmark: 'd',
toggleCollapse: 'shift+x',
toggleSensitive: 'h',
openMedia: 'e',
};

class SwitchingColumnsArea extends React.PureComponent {
@@ -254,6 +257,7 @@ class UI extends React.Component {
dropdownMenuIsOpen: PropTypes.bool,
unreadNotifications: PropTypes.number,
showFaviconBadge: PropTypes.bool,
moved: PropTypes.map,
};

state = {
@@ -539,7 +543,7 @@ class UI extends React.Component {

render () {
const { draggingOver } = this.state;
const { children, layout, isWide, navbarUnder, location, dropdownMenuIsOpen } = this.props;
const { children, layout, isWide, navbarUnder, location, dropdownMenuIsOpen, moved } = this.props;

const columnsClass = layout => {
switch (layout) {
@@ -583,6 +587,17 @@ class UI extends React.Component {
return (
<HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef} attach={window} focused>
<div className={className} ref={this.setRef} style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}>
{moved && (<div className='flash-message alert'>
<FormattedMessage
id='moved_to_warning'
defaultMessage='This account is marked as moved to {moved_to_link}, and may thus not accept new follows.'
values={{ moved_to_link: (
<PermaLink href={moved.get('url')} to={`/accounts/${moved.get('id')}`}>
@{moved.get('acct')}
</PermaLink>
)}}
/>
</div>)}
<SwitchingColumnsArea location={location} layout={layout} navbarUnder={navbarUnder} onLayoutChange={this.handleLayoutChange}>
{children}
</SwitchingColumnsArea>

+ 1
- 1
app/javascript/flavours/glitch/features/video/index.js View File

@@ -488,7 +488,7 @@ class Video extends React.PureComponent {

<div className='video-player__buttons-bar'>
<div className='video-player__buttons left'>
<button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
<button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay} autoFocus={detailed}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
<button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>
<div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>
&nbsp;

+ 1
- 1
app/javascript/flavours/glitch/reducers/compose.js View File

@@ -429,7 +429,7 @@ export default function compose(state = initialState, action) {
case COMPOSE_UPLOAD_SUCCESS:
return appendMedia(state, fromJS(action.media), action.file);
case COMPOSE_UPLOAD_FAIL:
return state.set('is_uploading', false).update('pending_media_attachments', n => action.decrement ? n - 1 : n);
return state.set('is_uploading', false).update('pending_media_attachments', n => n - 1);
case COMPOSE_UPLOAD_UNDO:
return removeMedia(state, action.media_id);
case COMPOSE_UPLOAD_PROGRESS:

+ 34
- 0
app/javascript/flavours/glitch/styles/components/index.scss View File

@@ -1507,6 +1507,16 @@
flex: 0 0 auto;
padding: 10px;
padding-top: 12px;
position: relative;
}

&__unread {
display: inline-block;
background: $highlight-text-color;
border-radius: 50%;
width: 0.625rem;
height: 0.625rem;
margin: -.1ex .15em .1ex;
}

&__content {
@@ -1554,6 +1564,30 @@
margin: 0;
}
}

&--unread {
background: lighten($ui-base-color, 2%);

&:focus {
background: lighten($ui-base-color, 4%);
}

.conversation__content__info {
font-weight: 700;
}

.conversation__content__relative-time {
color: $primary-text-color;
}
}
}

.ui .flash-message {
margin-top: 10px;
margin-left: auto;
margin-right: auto;
margin-bottom: 0;
min-width: 75%;
}

::-webkit-scrollbar-thumb {

+ 1
- 1
app/javascript/flavours/glitch/styles/containers.scss View File

@@ -652,7 +652,7 @@
}

.counter {
width: 33.3%;
min-width: 33.3%;
box-sizing: border-box;
flex: 0 0 auto;
color: $darker-text-color;

+ 7
- 1
app/javascript/flavours/glitch/styles/forms.scss View File

@@ -504,6 +504,10 @@ code {
&__overlay-area {
position: relative;

&__blurred form {
filter: blur(2px);
}

&__overlay {
position: absolute;
top: 0;
@@ -514,8 +518,10 @@ code {
justify-content: center;
align-items: center;
background: rgba($ui-base-color, 0.65);
backdrop-filter: blur(2px);
border-radius: 4px;
margin-left: -4px;
margin-top: -4px;
padding: 4px;

&__content {
text-align: center;

+ 2
- 3
app/javascript/mastodon/actions/compose.js View File

@@ -236,7 +236,7 @@ export function uploadCompose(files) {
dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
},
}).then(({ data }) => dispatch(uploadComposeSuccess(data, f)));
}).catch(error => dispatch(uploadComposeFail(error, true)));
}).catch(error => dispatch(uploadComposeFail(error)));
};
};
};
@@ -267,11 +267,10 @@ export function changeUploadComposeSuccess(media) {
};
};

export function changeUploadComposeFail(error, decrement = false) {
export function changeUploadComposeFail(error) {
return {
type: COMPOSE_UPLOAD_CHANGE_FAIL,
error: error,
decrement: decrement,
skipLoading: true,
};
};

+ 10
- 4
app/javascript/mastodon/components/modal_root.js View File

@@ -56,15 +56,21 @@ export default class ModalRoot extends React.PureComponent {
} else if (!nextProps.children) {
this.setState({ revealed: false });
}
if (!nextProps.children && !!this.props.children) {
this.activeElement.focus();
this.activeElement = null;
}
}

componentDidUpdate (prevProps) {
if (!this.props.children && !!prevProps.children) {
this.getSiblings().forEach(sibling => sibling.removeAttribute('inert'));

// Because of the wicg-inert polyfill, the activeElement may not be
// immediately selectable, we have to wait for observers to run, as
// described in https://github.com/WICG/inert#performance-and-gotchas
Promise.resolve().then(() => {
this.activeElement.focus();
this.activeElement = null;
}).catch((error) => {
console.error(error);
});
}
if (this.props.children) {
requestAnimationFrame(() => {

+ 17
- 0
app/javascript/mastodon/components/status.js View File

@@ -214,6 +214,22 @@ class Status extends ImmutablePureComponent {
this.props.onOpenVideo(media, startTime);
}

handleHotkeyOpenMedia = e => {
const { status, onOpenMedia, onOpenVideo } = this.props;

e.preventDefault();

if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
// TODO: toggle play/paused?
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
onOpenVideo(status.getIn(['media_attachments', 0]), 0);
} else {
onOpenMedia(status.get('media_attachments'), 0);
}
}
}

handleHotkeyReply = e => {
e.preventDefault();
this.props.onReply(this._properStatus(), this.context.router.history);
@@ -293,6 +309,7 @@ class Status extends ImmutablePureComponent {
moveDown: this.handleHotkeyMoveDown,
toggleHidden: this.handleHotkeyToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive,
openMedia: this.handleHotkeyOpenMedia,
};

if (hidden) {

+ 1
- 1
app/javascript/mastodon/features/compose/components/poll_form.js View File

@@ -144,7 +144,7 @@ class PollForm extends ImmutablePureComponent {
<div className='poll__footer'>
<button disabled={options.size >= 5} className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button>

<select value={expiresIn} onChange={this.handleSelectDuration}>
<select value={expiresIn} onBlur={this.handleSelectDuration}>
<option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
<option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
<option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option>

+ 3
- 2
app/javascript/mastodon/features/direct_timeline/components/conversation.js View File

@@ -12,6 +12,7 @@ import IconButton from 'mastodon/components/icon_button';
import RelativeTimestamp from 'mastodon/components/relative_timestamp';
import { HotKeys } from 'react-hotkeys';
import { autoPlayGif } from 'mastodon/initial_state';
import classNames from 'classnames';

const messages = defineMessages({
more: { id: 'status.more', defaultMessage: 'More' },
@@ -158,7 +159,7 @@ class Conversation extends ImmutablePureComponent {

return (
<HotKeys handlers={handlers}>
<div className='conversation focusable muted' tabIndex='0'>
<div className={classNames('conversation focusable muted', { 'conversation--unread': unread })} tabIndex='0'>
<div className='conversation__avatar'>
<AvatarComposite accounts={accounts} size={48} />
</div>
@@ -166,7 +167,7 @@ class Conversation extends ImmutablePureComponent {
<div className='conversation__content'>
<div className='conversation__content__info'>
<div className='conversation__content__relative-time'>
<RelativeTimestamp timestamp={lastStatus.get('created_at')} />
{unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} />
</div>

<div className='conversation__content__names' ref={this.setNamesRef}>

+ 4
- 0
app/javascript/mastodon/features/keyboard_shortcuts/index.js View File

@@ -57,6 +57,10 @@ class KeyboardShortcuts extends ImmutablePureComponent {
<td><FormattedMessage id='keyboard_shortcuts.enter' defaultMessage='to open status' /></td>
</tr>
<tr>
<td><kbd>e</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.open_media' defaultMessage='to open media' /></td>
</tr>
<tr>
<td><kbd>x</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.toggle_hidden' defaultMessage='to show/hide text behind CW' /></td>
</tr>

+ 17
- 0
app/javascript/mastodon/features/status/index.js View File

@@ -281,6 +281,22 @@ class Status extends ImmutablePureComponent {
this.props.dispatch(openModal('VIDEO', { media, time }));
}

handleHotkeyOpenMedia = e => {
const { status } = this.props;

e.preventDefault();

if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
// TODO: toggle play/paused?
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
this.handleOpenVideo(status.getIn(['media_attachments', 0]), 0);
} else {
this.handleOpenMedia(status.get('media_attachments'), 0);
}
}
}

handleMuteClick = (account) => {
this.props.dispatch(initMuteModal(account));
}
@@ -506,6 +522,7 @@ class Status extends ImmutablePureComponent {
openProfile: this.handleHotkeyOpenProfile,
toggleHidden: this.handleHotkeyToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive,
openMedia: this.handleHotkeyOpenMedia,
};

return (

+ 14
- 2
app/javascript/mastodon/features/ui/components/focal_point_modal.js View File

@@ -214,7 +214,7 @@ class FocalPointModal extends ImmutablePureComponent {
langPath: `${assetHost}/ocr/lang-data`,
});

let media_url = media.get('file');
let media_url = media.get('url');

if (window.URL && URL.createObjectURL) {
try {
@@ -244,6 +244,16 @@ class FocalPointModal extends ImmutablePureComponent {
const previewWidth = 200;
const previewHeight = previewWidth / previewRatio;

let descriptionLabel = null;

if (media.get('type') === 'audio') {
descriptionLabel = <FormattedMessage id='upload_form.audio_description' defaultMessage='Describe for people with hearing loss' />;
} else if (media.get('type') === 'video') {
descriptionLabel = <FormattedMessage id='upload_form.video_description' defaultMessage='Describe for people with hearing loss or visual impairment' />;
} else {
descriptionLabel = <FormattedMessage id='upload_form.description' defaultMessage='Describe for the visually impaired' />;
}

return (
<div className='modal-root__modal report-modal' style={{ maxWidth: 960 }}>
<div className='report-modal__target'>
@@ -255,7 +265,9 @@ class FocalPointModal extends ImmutablePureComponent {
<div className='report-modal__comment'>
{focals && <p><FormattedMessage id='upload_modal.hint' defaultMessage='Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.' /></p>}

<label className='setting-text-label' htmlFor='upload-modal__description'><FormattedMessage id='upload_form.description' defaultMessage='Describe for the visually impaired' /></label>
<label className='setting-text-label' htmlFor='upload-modal__description'>
{descriptionLabel}
</label>

<div className='setting-text__wrapper'>
<Textarea

+ 1
- 0
app/javascript/mastodon/features/ui/index.js View File

@@ -100,6 +100,7 @@ const keyMap = {
goToRequests: 'g r',
toggleHidden: 'x',
toggleSensitive: 'h',
openMedia: 'e',
};

class SwitchingColumnsArea extends React.PureComponent {

+ 1
- 1
app/javascript/mastodon/features/video/index.js View File

@@ -467,7 +467,7 @@ class Video extends React.PureComponent {

<div className='video-player__buttons-bar'>
<div className='video-player__buttons left'>
<button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
<button type='button' aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} onClick={this.togglePlay} autoFocus={detailed}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button>
<button type='button' aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button>

<div className='video-player__volume' onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}>

+ 1
- 1
app/javascript/mastodon/reducers/compose.js View File

@@ -328,7 +328,7 @@ export default function compose(state = initialState, action) {
case COMPOSE_UPLOAD_SUCCESS:
return appendMedia(state, fromJS(action.media), action.file);
case COMPOSE_UPLOAD_FAIL:
return state.set('is_uploading', false).update('pending_media_attachments', n => action.decrement ? n - 1 : n);
return state.set('is_uploading', false).update('pending_media_attachments', n => n - 1);
case COMPOSE_UPLOAD_UNDO:
return removeMedia(state, action.media_id);
case COMPOSE_UPLOAD_PROGRESS:

+ 26
- 0
app/javascript/styles/mastodon/components.scss View File

@@ -6517,6 +6517,16 @@ noscript {
flex: 0 0 auto;
padding: 10px;
padding-top: 12px;
position: relative;
}

&__unread {
display: inline-block;
background: $highlight-text-color;
border-radius: 50%;
width: 0.625rem;
height: 0.625rem;
margin: -.1ex .15em .1ex;
}

&__content {
@@ -6564,4 +6574,20 @@ noscript {
word-break: break-word;
}
}

&--unread {
background: lighten($ui-base-color, 2%);

&:focus {
background: lighten($ui-base-color, 4%);
}

.conversation__content__info {
font-weight: 700;
}

.conversation__content__relative-time {
color: $primary-text-color;
}
}
}

+ 1
- 1
app/javascript/styles/mastodon/containers.scss View File

@@ -646,7 +646,7 @@
}

.counter {
width: 33.3%;
min-width: 33.3%;
box-sizing: border-box;
flex: 0 0 auto;
color: $darker-text-color;

+ 7
- 1
app/javascript/styles/mastodon/forms.scss View File

@@ -513,6 +513,10 @@ code {
&__overlay-area {
position: relative;

&__blurred form {
filter: blur(2px);
}

&__overlay {
position: absolute;
top: 0;
@@ -523,8 +527,10 @@ code {
justify-content: center;
align-items: center;
background: rgba($ui-base-color, 0.65);
backdrop-filter: blur(2px);
border-radius: 4px;
margin-left: -4px;
margin-top: -4px;
padding: 4px;

&__content {
text-align: center;

+ 14
- 6
app/lib/activitypub/activity/create.rb View File

@@ -25,6 +25,14 @@ class ActivityPub::Activity::Create < ActivityPub::Activity

private

def audience_to
@object['to'] || @json['to']
end

def audience_cc
@object['cc'] || @json['cc']
end

def process_status
@tags = []
@mentions = []
@@ -78,7 +86,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
end

def process_audience
(as_array(@object['to']) + as_array(@object['cc'])).uniq.each do |audience|
(as_array(audience_to) + as_array(audience_cc)).uniq.each do |audience|
next if audience == ActivityPub::TagManager::COLLECTIONS[:public]

# Unlike with tags, there is no point in resolving accounts we don't already
@@ -294,11 +302,11 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
end

def visibility_from_audience
if equals_or_includes?(@object['to'], ActivityPub::TagManager::COLLECTIONS[:public])
if equals_or_includes?(audience_to, ActivityPub::TagManager::COLLECTIONS[:public])
:public
elsif equals_or_includes?(@object['cc'], ActivityPub::TagManager::COLLECTIONS[:public])
elsif equals_or_includes?(audience_cc, ActivityPub::TagManager::COLLECTIONS[:public])
:unlisted
elsif equals_or_includes?(@object['to'], @account.followers_url)
elsif equals_or_includes?(audience_to, @account.followers_url)
:private
else
:direct
@@ -307,7 +315,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity

def audience_includes?(account)
uri = ActivityPub::TagManager.instance.uri_for(account)
equals_or_includes?(@object['to'], uri) || equals_or_includes?(@object['cc'], uri)
equals_or_includes?(audience_to, uri) || equals_or_includes?(audience_cc, uri)
end

def replied_to_status
@@ -418,7 +426,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
def addresses_local_accounts?
return true if @options[:delivered_to_account_id]

local_usernames = (as_array(@object['to']) + as_array(@object['cc'])).uniq.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }.map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) }
local_usernames = (as_array(audience_to) + as_array(audience_cc)).uniq.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }.map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) }

return false if local_usernames.empty?


+ 2
- 2
app/models/account.rb View File

@@ -464,7 +464,7 @@ class Account < ApplicationRecord
accounts.*,
(count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
FROM accounts
LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?)
LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?)
WHERE accounts.id IN (SELECT * FROM first_degree)
AND #{query} @@ #{textsearch}
AND accounts.suspended_at IS NULL
@@ -474,7 +474,7 @@ class Account < ApplicationRecord
LIMIT ? OFFSET ?
SQL

records = find_by_sql([sql, account.id, account.id, account.id, account.id, limit, offset])
records = find_by_sql([sql, account.id, account.id, account.id, limit, offset])
else
sql = <<-SQL.squish
SELECT

+ 10
- 2
app/models/concerns/ldap_authenticable.rb View File

@@ -14,10 +14,18 @@ module LdapAuthenticable
end

def ldap_get_user(attributes = {})
resource = joins(:account).find_by(accounts: { username: attributes[Devise.ldap_uid.to_sym].first })
safe_username = attributes[Devise.ldap_uid.to_sym].first
if Devise.ldap_uid_conversion_enabled
keys = Regexp.union(Devise.ldap_uid_conversion_search.chars)
replacement = Devise.ldap_uid_conversion_replace

safe_username = safe_username.gsub(keys, replacement)
end

resource = joins(:account).find_by(accounts: { username: safe_username })

if resource.blank?
resource = new(email: attributes[:mail].first, agreement: true, account_attributes: { username: attributes[Devise.ldap_uid.to_sym].first }, admin: false, external: true, confirmed_at: Time.now.utc)
resource = new(email: attributes[:mail].first, agreement: true, account_attributes: { username: safe_username }, admin: false, external: true, confirmed_at: Time.now.utc)
resource.save!
end


+ 2
- 0
app/models/report_filter.rb View File

@@ -19,6 +19,8 @@ class ReportFilter

def scope_for(key, value)
case key.to_sym
when :by_target_domain
Report.where(target_account: Account.where(domain: value))
when :resolved
Report.resolved
when :account_id

+ 1
- 1
app/models/user.rb View File

@@ -170,7 +170,7 @@ class User < ApplicationRecord
end

def functional?
confirmed? && approved? && !disabled? && !account.suspended? && account.moved_to_account_id.nil?
confirmed? && approved? && !disabled? && !account.suspended?
end

def unconfirmed_or_pending?

+ 2
- 1
app/presenters/status_relationships_presenter.rb View File

@@ -1,7 +1,8 @@
# frozen_string_literal: true

class StatusRelationshipsPresenter
attr_reader :reblogs_map, :favourites_map, :mutes_map, :pins_map
attr_reader :reblogs_map, :favourites_map, :mutes_map, :pins_map,
:bookmarks_map

def initialize(statuses, current_account_id = nil, **options)
if current_account_id.nil?

+ 2
- 2
app/serializers/rest/status_serializer.rb View File

@@ -97,8 +97,8 @@ class REST::StatusSerializer < ActiveModel::Serializer
end

def bookmarked
if instance_options && instance_options[:bookmarks]
instance_options[:bookmarks].bookmarks_map[object.id] || false
if instance_options && instance_options[:relationships]
instance_options[:relationships].bookmarks_map[object.id] || false
else
current_user.account.bookmarked?(object)
end

+ 1
- 1
app/services/activitypub/process_poll_service.rb View File

@@ -30,7 +30,7 @@ class ActivityPub::ProcessPollService < BaseService

voters_count = @json['votersCount']

latest_options = items.map { |item| item['name'].presence || item['content'] }
latest_options = items.map { |item| item['name'].presence || item['content'] }.compact

# If for some reasons the options were changed, it invalidates all previous
# votes, so we need to remove them

+ 2
- 0
app/services/block_domain_service.rb View File

@@ -27,6 +27,8 @@ class BlockDomainService < BaseService
elsif domain_block.suspend?
suspend_accounts!
end

clear_media! if domain_block.reject_media?
end

def invalidate_association_caches!

+ 1
- 1
app/services/fetch_link_card_service.rb View File

@@ -67,7 +67,7 @@ class FetchLinkCardService < BaseService
else
html = Nokogiri::HTML(@status.text)
links = html.css('a')
urls = links.map { |a| Addressable::URI.parse(a['href']).normalize unless skip_link?(a) }.compact
urls = links.map { |a| Addressable::URI.parse(a['href']) unless skip_link?(a) }.compact.map(&:normalize).compact
end

urls.reject { |uri| bad_url?(uri) }.first

+ 1
- 1
app/services/fetch_oembed_service.rb View File

@@ -58,7 +58,7 @@ class FetchOEmbedService
url_domain = Addressable::URI.parse(@url).normalized_host

endpoint_hash = {
endpoint: @endpoint_url.gsub(URI.encode_www_form_component(@url), '{url}'),
endpoint: @endpoint_url.gsub(/(=(http[s]?(%3A|:)(\/\/|%2F%2F)))([^&]*)/i, '={url}'),
format: @format,
}


+ 7
- 7
app/views/about/_registration.html.haml View File

@@ -1,5 +1,5 @@
= simple_form_for(new_user, url: user_registration_path, namespace: 'registration') do |f|
.simple_form__overlay-area
.simple_form__overlay-area{ class: (closed_registrations? && @instance_presenter.closed_registrations_message.present?) ? 'simple_form__overlay-area__blurred' : '' }
= simple_form_for(new_user, url: user_registration_path, namespace: 'registration') do |f|
%p.lead= t('about.federation_hint_html', instance: content_tag(:strong, site_hostname))

.fields-group
@@ -21,8 +21,8 @@
.actions
= f.button :button, sign_up_message, type: :submit, class: 'button button-primary', disabled: closed_registrations?

- if closed_registrations? && @instance_presenter.closed_registrations_message.present?
.simple_form__overlay-area__overlay
.simple_form__overlay-area__overlay__content.rich-formatting
.block-icon= fa_icon 'warning'
= @instance_presenter.closed_registrations_message.html_safe
- if closed_registrations? && @instance_presenter.closed_registrations_message.present?
.simple_form__overlay-area__overlay
.simple_form__overlay-area__overlay__content.rich-formatting
.block-icon= fa_icon 'warning'
= @instance_presenter.closed_registrations_message.html_safe

+ 14
- 0
app/views/admin/reports/index.html.haml View File

@@ -8,6 +8,20 @@
%li= filter_link_to t('admin.reports.unresolved'), resolved: nil
%li= filter_link_to t('admin.reports.resolved'), resolved: '1'

= form_tag admin_reports_url, method: 'GET', class: 'simple_form' do
.fields-group
- Admin::FilterHelper::REPORT_FILTERS.each do |key|
- if params[key].present?
= hidden_field_tag key, params[key]

- %i(by_target_domain).each do |key|
.input.string.optional
= text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.reports.#{key}")

.actions
%button= t('admin.accounts.search')
= link_to t('admin.accounts.reset'), admin_reports_path, class: 'button negative'

- @reports.group_by(&:target_account_id).each do |target_account_id, reports|
- target_account = reports.first.target_account
.report-card

+ 2
- 2
app/views/relationships/show.html.haml View File

@@ -5,8 +5,8 @@
.filter-subset
%strong= t 'relationships.relationship'
%ul
%li= filter_link_to t('accounts.following', count: current_account.following_count), relationship: nil
%li= filter_link_to t('accounts.followers', count: current_account.followers_count), relationship: 'followed_by'
%li= filter_link_to t('relationships.following'), relationship: nil
%li= filter_link_to t('relationships.followers'), relationship: 'followed_by'
%li= filter_link_to t('relationships.mutual'), relationship: 'mutual'

.filter-subset

+ 6
- 0
app/views/settings/preferences/notifications/show.html.haml View File

@@ -4,6 +4,10 @@
= simple_form_for current_user, url: settings_preferences_notifications_path, html: { method: :put } do |f|
= render 'shared/error_messages', object: current_user

%h4= t('notifications.email_events')

%p.hint = t('notifications.email_events_hint')

.fields-group
= f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff|
= ff.input :follow, as: :boolean, wrapper: :with_label
@@ -21,6 +25,8 @@
= f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff|
= ff.input :digest, as: :boolean, wrapper: :with_label

%h4 = t('notifications.other_settings')

.fields-group
= f.simple_fields_for :interactions, hash_to_object(current_user.settings.interactions) do |ff|
= ff.input :must_be_follower, as: :boolean, wrapper: :with_label

+ 9
- 0
config/initializers/devise.rb View File

@@ -61,6 +61,12 @@ module Devise
@@ldap_tls_no_verify = false
mattr_accessor :ldap_search_filter
@@ldap_search_filter = nil
mattr_accessor :ldap_uid_conversion_enabled
@@ldap_uid_conversion_enabled = false
mattr_accessor :ldap_uid_conversion_search
@@ldap_uid_conversion_search = nil
mattr_accessor :ldap_uid_conversion_replace
@@ldap_uid_conversion_replace = nil

class Strategies::PamAuthenticatable
def valid?
@@ -365,5 +371,8 @@ Devise.setup do |config|
config.ldap_uid = ENV.fetch('LDAP_UID', 'cn')
config.ldap_tls_no_verify = ENV['LDAP_TLS_NO_VERIFY'] == 'true'
config.ldap_search_filter = ENV.fetch('LDAP_SEARCH_FILTER', '%{uid}=%{email}')
config.ldap_uid_conversion_enabled = ENV['LDAP_UID_CONVERSION_ENABLED'] == 'true'
config.ldap_uid_conversion_search = ENV.fetch('LDAP_UID_CONVERSION_SEARCH', '.,- ')
config.ldap_uid_conversion_replace = ENV.fetch('LDAP_UID_CONVERSION_REPLACE', '_')
end
end

+ 14
- 2
config/initializers/doorkeeper.rb View File

@@ -8,8 +8,20 @@ Doorkeeper.configure do
end

resource_owner_from_credentials do |_routes|
user = User.find_by(email: request.params[:username])
user if !user&.otp_required_for_login? && user&.valid_password?(request.params[:password])
if Devise.ldap_authentication
user = User.authenticate_with_ldap({ :email => request.params[:username], :password => request.params[:password] })
end

if Devise.pam_authentication
user ||= User.authenticate_with_ldap({ :email => request.params[:username], :password => request.params[:password] })
end

if user.nil?
user = User.find_by(email: request.params[:username])
user = nil unless user.valid_password?(request.params[:password])
end

user if !user&.otp_required_for_login?
end

# If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below.

+ 7
- 0
config/locales/en.yml View File

@@ -429,6 +429,7 @@ en:
are_you_sure: Are you sure?
assign_to_self: Assign to me
assigned: Assigned moderator
by_target_domain: Domain of reported account
comment:
none: None
created_at: Reported
@@ -923,6 +924,10 @@ en:
body: 'Your status was boosted by %{name}:'
subject: "%{name} boosted your status"
title: New boost
notifications:
email_events: Events for e-mail notifications
email_events_hint: 'Select events that you want to receive notifications for:'
other_settings: Other notifications settings
number:
human:
decimal_units:
@@ -957,6 +962,8 @@ en:
relationships:
activity: Account activity
dormant: Dormant
followers: Followers
following: Following
last_active: Last active
most_recent: Most recent
moved: Moved

+ 8
- 8
config/locales/simple_form.en.yml View File

@@ -172,14 +172,14 @@ en:
text: Why do you want to join?
notification_emails:
digest: Send digest e-mails
favourite: Send e-mail when someone favourites your status
follow: Send e-mail when someone follows you
follow_request: Send e-mail when someone requests to follow you
mention: Send e-mail when someone mentions you
pending_account: Send e-mail when a new account needs review
reblog: Send e-mail when someone boosts your status
report: Send e-mail when a new report is submitted
trending_tag: Send e-mail when an unreviewed hashtag is trending
favourite: Someone favourited your status
follow: Someone followed you
follow_request: Someone requested to follow you
mention: Someone mentioned you
pending_account: New account needs review
reblog: Someone boosted your status
report: New report is submitted
trending_tag: An unreviewed hashtag is trending
tag:
listable: Allow this hashtag to appear in searches and on the profile directory
name: Hashtag

+ 41
- 0
config/pghero.yml View File

@@ -0,0 +1,41 @@
databases:
primary:
# Database URL (defaults to app database)
# url: <%= ENV["DATABASE_URL"] %>

# Add more databases
# other:
# url: <%= ENV["OTHER_DATABASE_URL"] %>

# Minimum time for long running queries
# long_running_query_sec: 60

# Minimum average time for slow queries
# slow_query_ms: 20

# Minimum calls for slow queries
# slow_query_calls: 100

# Minimum connections for high connections warning
# total_connections_threshold: 500

# Statement timeout for explain
# explain_timeout_sec: 10

# Time zone (defaults to app time zone)
# time_zone: "Pacific Time (US & Canada)"

# Basic authentication
# username: admin
# password: secret

# Stats database URL (defaults to app database)
# stats_database_url: <%= ENV["PGHERO_STATS_DATABASE_URL"] %>

# AWS configuration (defaults to app AWS config)
# also need aws_db_instance_identifier with each database
# aws_access_key_id: ...
# aws_secret_access_key: ...
# aws_region: us-east-1

override_csp: true

+ 7
- 7
package.json View File

@@ -64,14 +64,14 @@
"@babel/plugin-proposal-class-properties": "^7.7.0",
"@babel/plugin-proposal-decorators": "^7.7.0",
"@babel/plugin-proposal-object-rest-spread": "^7.6.2",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@babel/plugin-transform-react-inline-elements": "^7.2.0",
"@babel/plugin-transform-react-jsx-self": "^7.2.0",
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
"@babel/plugin-transform-react-inline-elements": "^7.7.4",
"@babel/plugin-transform-react-jsx-self": "^7.7.4",
"@babel/plugin-transform-react-jsx-source": "^7.5.0",
"@babel/plugin-transform-runtime": "^7.5.5",
"@babel/preset-env": "^7.7.1",
"@babel/plugin-transform-runtime": "^7.7.4",
"@babel/preset-env": "^7.7.4",
"@babel/preset-react": "^7.7.0",
"@babel/runtime": "^7.7.2",
"@babel/runtime": "^7.7.4",
"@clusterws/cws": "^0.16.0",
"array-includes": "^3.0.3",
"atrament": "^0.2.3",
@@ -161,7 +161,7 @@
"stringz": "^2.0.0",
"substring-trie": "^1.0.2",
"terser-webpack-plugin": "^2.2.1",
"tesseract.js": "^2.0.0-beta.2",
"tesseract.js": "^2.0.0-alpha.16",
"throng": "^4.0.0",
"tiny-queue": "^0.2.1",
"uuid": "^3.3.3",

+ 1
- 1
spec/services/fetch_link_card_service_spec.rb View File

@@ -80,7 +80,7 @@ RSpec.describe FetchLinkCardService, type: :service do
end

context 'in a remote status' do
let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com'), text: 'Habt ihr ein paar gute Links zu #<span class="tag"><a href="https://quitter.se/tag/wannacry" target="_blank" rel="tag noopener noreferrer" title="https://quitter.se/tag/wannacry">Wannacry</a></span> herumfliegen? Ich will mal unter <br> <a href="https://github.com/qbi/WannaCry" target="_blank" rel="noopener noreferrer" title="https://github.com/qbi/WannaCry">https://github.com/qbi/WannaCry</a> was sammeln. !<a href="http://sn.jonkman.ca/group/416/id" target="_blank" rel="noopener noreferrer" title="http://sn.jonkman.ca/group/416/id">security</a>&nbsp;') }
let(:status) { Fabricate(:status, account: Fabricate(:account, domain: 'example.com'), text: 'Habt ihr ein paar gute Links zu <a>foo</a> #<span class="tag"><a href="https://quitter.se/tag/wannacry" target="_blank" rel="tag noopener noreferrer" title="https://quitter.se/tag/wannacry">Wannacry</a></span> herumfliegen? Ich will mal unter <br> <a href="https://github.com/qbi/WannaCry" target="_blank" rel="noopener noreferrer" title="https://github.com/qbi/WannaCry">https://github.com/qbi/WannaCry</a> was sammeln. !<a href="http://sn.jonkman.ca/group/416/id" target="_blank" rel="noopener noreferrer" title="http://sn.jonkman.ca/group/416/id">security</a>&nbsp;') }

it 'parses out URLs' do
expect(a_request(:get, 'https://github.com/qbi/WannaCry')).to have_been_made.at_least_once

+ 474
- 449
yarn.lock
File diff suppressed because it is too large
View File


Loading…
Cancel
Save