Compare commits

...

28 Commits

Author SHA1 Message Date
Eugen Rochko
90712d4293 Fix errors preventing UnsubscribeService from working (#4866) 2017-09-09 17:36:27 +02:00
Eugen Rochko
6867681c7c Add script to make embedded iframes autosize (#4853) 2017-09-09 16:23:44 +02:00
Eugen Rochko
bdc8b4fd91 Disable mouse-based pause from #4859 (#4865)
It wasn't working ideally and introduced some annoying false positivies
2017-09-09 15:09:50 +02:00
Eugen Rochko
2ff7146b6d Bump version to 1.6.0rc4 2017-09-09 14:53:49 +02:00
unarist
c7908e2d09 Fix scroll behavior and others on paused timeline (#4864)
Resolved:

* Lot of redundant renders while mouse moving
* Scroll jumping when timeline loaded
* Scroll position isn't kept when statuses below the scrollTop was deleted then new status arrived

Unresolved:

* Scroll position isn't kept when statuses over the scrollTop was deleted then new status arrived
-> It needs to know which statuses are over the scrollTop
* New status indicator should be active when new statuses arrived while mouse moved recently
-> It needs a) update indicator in ScrollableList, or b) set scrollTop status while mouse moving
2017-09-09 14:16:11 +02:00
Yamagishi Kazutoshi
c9d04f1c39 Fix second report (regression from 3b81baaaaf) (#4863) 2017-09-09 13:42:48 +02:00
Eugen Rochko
9e15eeec63 Add missing reject_media check before avatar download via ActivityPub (#4862) 2017-09-09 13:41:45 +02:00
Lynx Kotoura
3c45d3963a Scrollable tables in settings pages (#4857)
* Scrollable tables in settings pages

* Add space before curly brace
2017-09-09 02:26:58 +02:00
Eugen Rochko
baa8b82179 Fix #1004 - Temporarily pause timeline if there's been recent mouse movement (#4859) 2017-09-09 02:26:41 +02:00
Eugen Rochko
4b460bc571 Fix #4852 - Check if already requested from FollowService (#4855) 2017-09-09 02:02:44 +02:00
Eugen Rochko
7ca173be47 Fix #4850 - When visibility missing from API call to toot, fallback to user preference (#4861) 2017-09-09 02:02:29 +02:00
unarist
1ae5d49a71 Refresh timeline after toot while the timeline is disconnected (#4858)
To reflect status posting immediately, we've inserted the status into timelines directly. However, status insertion changes "latest status", and it means next timeline refresh only fetches statuses since the inserted status. This behavior is very bad for disconnected timeline and mobile views.

After this patch, it refreshes timeline for disconnected timelines, instead of direct insertion.
2017-09-08 21:43:34 +02:00
unarist
a12572e074 Handle stream_entry URL correctly in ActivityPub (#4854)
In before, the method uses stream_entry id as status id, so replied status was wrongly selected.

This PR uses StatusFinder which was introduced with `Api::Web::EmbedsController`.
2017-09-08 18:20:03 +02:00
Quent-in
dabc309ca3 i10n update OC and FR (#4849)
* Missing "navigation_bar.pins"

* Missing "navigation_bar.pins"
2017-09-08 13:55:47 +02:00
Eugen Rochko
1caf11ddcc Fix language filter codes (#4841)
* Fix language filter codes

CLD3 returns BCP-47 language identifier, filter settings expect
identifiers in the ISO 639-1 format. Convert between formats,
and exclude duplicate languages from filter choices (zh-CN->zh)

* Fix zh name
2017-09-08 12:32:22 +02:00
Eugen Rochko
95f018a3d4 "Mute conversation" option on all own toots, not just in notifications (#4844)
That way you can mute notifications for a toot before you get replies
to it or boosts or favourites
2017-09-08 12:00:30 +02:00
Eugen Rochko
a4caa7eb62 Fetch statuses/following/followers numbers from ActivityPub collections (#4840) 2017-09-08 12:00:17 +02:00
m4sk1n
7c2d84910c i18n: Update Polish translation (#4845)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2017-09-08 05:51:48 +09:00
Quent-in
b00cc4b9bd i10n OC / FR update Pinned toots (#4842)
* Added column.pins

New strings

* Added column.pins

* Update confirmation_instructions.oc.html.erb

* Update confirmation_instructions.oc.text.erb

* Update password_change.oc.html.erb

* Update password_change.oc.text.erb

* Update reset_password_instructions.oc.html.erb

* Update reset_password_instructions.oc.text.erb

* Update confirmation_instructions.oc.html.erb

* Update confirmation_instructions.oc.text.erb
2017-09-07 22:07:03 +02:00
Eugen Rochko
dd6ede554f Fix #4834 - Adjust Status#local and Status#remote scopes (#4839) 2017-09-07 20:18:34 +02:00
abcang
6859d4c028 Enable UniqueRetryJobMiddleware even when called from sidekiq worker (#4836) 2017-09-07 16:44:14 +02:00
PFM
7d853b514a Use <button> instead of <div role="button"> (#4835) 2017-09-07 16:18:41 +02:00
voidSatisfaction
85c7c42098 Add Pinned toot column (#4817)
* Add Pinned_toot_section

* Fix add frozen_string_literal

* Fix delete no need controller and tests

* Fix replace query strings to axios params

* Fix change value to accountId and disabling more button
2017-09-07 09:58:11 +02:00
voidSatisfaction
8185f98872 Feat add validation for report comment: characters under 1000 valid (#4833) 2017-09-07 09:55:42 +02:00
Joseph Mingrone
5264496240 Use casecmp() instead of casecmp?() for now (#4832)
* Use casecmp() instead of casecmp?() for now

casecmp?() is only available in ruby 2.4.0.  Users running earlier ruby versions
would see errors, e.g., running
RAILS_ENV=production rails mastodon:maintenance:remove_deprecated_preview_cards.

* Correctly check whether casecmp() returns 0
2017-09-07 03:55:06 +02:00
Quent-in
be75b13d68 i10n update OC and FR files (#4824)
* Onboarding: corrections

Some missing letters and spaces or better wording

* Embed

Translated as Intégrer in FR / Embarcar in OC
2017-09-07 08:55:03 +09:00
Olivier Humbert
9417c9bb8f Update fr.json (#4830)
typo
2017-09-06 22:32:49 +02:00
Eugen Rochko
11bddd31ce Fix locking migration on statuses table. Nullable column and NO default value (#4825) 2017-09-06 20:57:52 +02:00
69 changed files with 700 additions and 366 deletions

View File

@@ -24,6 +24,7 @@ gem 'addressable', '~> 2.5'
gem 'bootsnap' gem 'bootsnap'
gem 'browser' gem 'browser'
gem 'charlock_holmes', '~> 0.7.5' gem 'charlock_holmes', '~> 0.7.5'
gem 'iso-639'
gem 'cld3', '~> 3.1' gem 'cld3', '~> 3.1'
gem 'devise', '~> 4.2' gem 'devise', '~> 4.2'
gem 'devise-two-factor', '~> 3.0' gem 'devise-two-factor', '~> 3.0'

View File

@@ -225,6 +225,7 @@ GEM
terminal-table (>= 1.5.1) terminal-table (>= 1.5.1)
idn-ruby (0.1.0) idn-ruby (0.1.0)
ipaddress (0.8.3) ipaddress (0.8.3)
iso-639 (0.2.8)
jmespath (1.3.1) jmespath (1.3.1)
json (2.1.0) json (2.1.0)
json-ld (2.1.5) json-ld (2.1.5)
@@ -560,6 +561,7 @@ DEPENDENCIES
httplog (~> 0.99) httplog (~> 0.99)
i18n-tasks (~> 0.9) i18n-tasks (~> 0.9)
idn-ruby idn-ruby
iso-639
json-ld-preloaded (~> 2.2.1) json-ld-preloaded (~> 2.2.1)
kaminari (~> 1.0) kaminari (~> 1.0)
letter_opener (~> 1.4) letter_opener (~> 1.4)

View File

@@ -30,6 +30,7 @@ module SettingsHelper
th: 'ภาษาไทย', th: 'ภาษาไทย',
tr: 'Türkçe', tr: 'Türkçe',
uk: 'Українська', uk: 'Українська',
zh: '中文',
'zh-CN': '简体中文', 'zh-CN': '简体中文',
'zh-HK': '繁體中文(香港)', 'zh-HK': '繁體中文(香港)',
'zh-TW': '繁體中文(臺灣)', 'zh-TW': '繁體中文(臺灣)',
@@ -39,6 +40,10 @@ module SettingsHelper
HUMAN_LOCALES[locale] HUMAN_LOCALES[locale]
end end
def filterable_languages
I18n.available_locales.map { |locale| locale.to_s.split('-').first.to_sym }.uniq
end
def hash_to_object(hash) def hash_to_object(hash)
HashObject.new(hash) HashObject.new(hash)
end end

View File

@@ -1,6 +1,11 @@
import api from '../api'; import api from '../api';
import { updateTimeline } from './timelines'; import {
updateTimeline,
refreshHomeTimeline,
refreshCommunityTimeline,
refreshPublicTimeline,
} from './timelines';
export const COMPOSE_CHANGE = 'COMPOSE_CHANGE'; export const COMPOSE_CHANGE = 'COMPOSE_CHANGE';
export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST'; export const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST';
@@ -95,16 +100,20 @@ export function submitCompose() {
dispatch(submitComposeSuccess({ ...response.data })); dispatch(submitComposeSuccess({ ...response.data }));
// To make the app more responsive, immediately get the status into the columns // To make the app more responsive, immediately get the status into the columns
dispatch(updateTimeline('home', { ...response.data }));
const insertOrRefresh = (timelineId, refreshAction) => {
if (getState().getIn(['timelines', timelineId, 'online'])) {
dispatch(updateTimeline(timelineId, { ...response.data }));
} else if (getState().getIn(['timelines', timelineId, 'loaded'])) {
dispatch(refreshAction());
}
};
insertOrRefresh('home', refreshHomeTimeline);
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') { if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
if (getState().getIn(['timelines', 'community', 'loaded'])) { insertOrRefresh('community', refreshCommunityTimeline);
dispatch(updateTimeline('community', { ...response.data })); insertOrRefresh('public', refreshPublicTimeline);
}
if (getState().getIn(['timelines', 'public', 'loaded'])) {
dispatch(updateTimeline('public', { ...response.data }));
}
} }
}).catch(function (error) { }).catch(function (error) {
dispatch(submitComposeFail(error)); dispatch(submitComposeFail(error));

View File

@@ -0,0 +1,39 @@
import api from '../api';
export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST';
export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS';
export const PINNED_STATUSES_FETCH_FAIL = 'PINNED_STATUSES_FETCH_FAIL';
export function fetchPinnedStatuses() {
return (dispatch, getState) => {
dispatch(fetchPinnedStatusesRequest());
const accountId = getState().getIn(['meta', 'me']);
api(getState).get(`/api/v1/accounts/${accountId}/statuses`, { params: { pinned: true } }).then(response => {
dispatch(fetchPinnedStatusesSuccess(response.data, null));
}).catch(error => {
dispatch(fetchPinnedStatusesFail(error));
});
};
};
export function fetchPinnedStatusesRequest() {
return {
type: PINNED_STATUSES_FETCH_REQUEST,
};
};
export function fetchPinnedStatusesSuccess(statuses, next) {
return {
type: PINNED_STATUSES_FETCH_SUCCESS,
statuses,
next,
};
};
export function fetchPinnedStatusesFail(error) {
return {
type: PINNED_STATUSES_FETCH_FAIL,
error,
};
};

View File

@@ -27,6 +27,10 @@ export default class ScrollableList extends PureComponent {
trackScroll: true, trackScroll: true,
}; };
state = {
lastMouseMove: null,
};
intersectionObserverWrapper = new IntersectionObserverWrapper(); intersectionObserverWrapper = new IntersectionObserverWrapper();
handleScroll = throttle(() => { handleScroll = throttle(() => {
@@ -47,6 +51,14 @@ export default class ScrollableList extends PureComponent {
trailing: true, trailing: true,
}); });
handleMouseMove = throttle(() => {
this._lastMouseMove = new Date();
}, 300);
handleMouseLeave = () => {
this._lastMouseMove = null;
}
componentDidMount () { componentDidMount () {
this.attachScrollListener(); this.attachScrollListener();
this.attachIntersectionObserver(); this.attachIntersectionObserver();
@@ -56,17 +68,20 @@ export default class ScrollableList extends PureComponent {
} }
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
const someItemInserted = React.Children.count(prevProps.children) > 0 &&
React.Children.count(prevProps.children) < React.Children.count(this.props.children) &&
this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props);
// Reset the scroll position when a new child comes in in order not to // Reset the scroll position when a new child comes in in order not to
// jerk the scrollbar around if you're already scrolled down the page. // jerk the scrollbar around if you're already scrolled down the page.
if (React.Children.count(prevProps.children) < React.Children.count(this.props.children) && this._oldScrollPosition && this.node.scrollTop > 0) { if (someItemInserted && this._oldScrollPosition && this.node.scrollTop > 0) {
if (this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props)) { const newScrollTop = this.node.scrollHeight - this._oldScrollPosition;
const newScrollTop = this.node.scrollHeight - this._oldScrollPosition;
if (this.node.scrollTop !== newScrollTop) { if (this.node.scrollTop !== newScrollTop) {
this.node.scrollTop = newScrollTop; this.node.scrollTop = newScrollTop;
}
} else {
this._oldScrollPosition = this.node.scrollHeight - this.node.scrollTop;
} }
} else {
this._oldScrollPosition = this.node.scrollHeight - this.node.scrollTop;
} }
} }
@@ -114,6 +129,10 @@ export default class ScrollableList extends PureComponent {
this.props.onScrollToBottom(); this.props.onScrollToBottom();
} }
_recentlyMoved () {
return this._lastMouseMove !== null && ((new Date()) - this._lastMouseMove < 600);
}
handleKeyDown = (e) => { handleKeyDown = (e) => {
if (['PageDown', 'PageUp'].includes(e.key) || (e.ctrlKey && ['End', 'Home'].includes(e.key))) { if (['PageDown', 'PageUp'].includes(e.key) || (e.ctrlKey && ['End', 'Home'].includes(e.key))) {
const article = (() => { const article = (() => {
@@ -149,7 +168,7 @@ export default class ScrollableList extends PureComponent {
if (isLoading || childrenCount > 0 || !emptyMessage) { if (isLoading || childrenCount > 0 || !emptyMessage) {
scrollableArea = ( scrollableArea = (
<div className='scrollable' ref={this.setRef}> <div className='scrollable' ref={this.setRef} onMouseMove={this.handleMouseMove} onMouseLeave={this.handleMouseLeave}>
<div role='feed' className='item-list' onKeyDown={this.handleKeyDown}> <div role='feed' className='item-list' onKeyDown={this.handleKeyDown}>
{prepend} {prepend}

View File

@@ -134,7 +134,7 @@ export default class StatusActionBar extends ImmutablePureComponent {
menu.push(null); menu.push(null);
if (withDismiss) { if (status.getIn(['account', 'id']) === me || withDismiss) {
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
menu.push(null); menu.push(null);
} }

View File

@@ -146,29 +146,29 @@ export default class VideoPlayer extends React.PureComponent {
if (!this.state.visible) { if (!this.state.visible) {
if (sensitive) { if (sensitive) {
return ( return (
<div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler__video' onClick={this.handleVisibility}> <button style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler' onClick={this.handleVisibility}>
{spoilerButton} {spoilerButton}
<span className='media-spoiler__warning'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span> <span className='media-spoiler__warning'><FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /></span>
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
</div> </button>
); );
} else { } else {
return ( return (
<div role='button' tabIndex='0' style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler__video' onClick={this.handleVisibility}> <button style={{ width: `${width}px`, height: `${height}px`, marginTop: '8px' }} className='media-spoiler' onClick={this.handleVisibility}>
{spoilerButton} {spoilerButton}
<span className='media-spoiler__warning'><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span> <span className='media-spoiler__warning'><FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' /></span>
<span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span> <span className='media-spoiler__trigger'><FormattedMessage id='status.sensitive_toggle' defaultMessage='Click to view' /></span>
</div> </button>
); );
} }
} }
if (this.state.preview && !autoplay) { if (this.state.preview && !autoplay) {
return ( return (
<div role='button' tabIndex='0' className='media-spoiler-video' style={{ width: `${width}px`, height: `${height}px`, backgroundImage: `url(${media.get('preview_url')})` }} onClick={this.handleOpen}> <button className='media-spoiler-video' style={{ width: `${width}px`, height: `${height}px`, backgroundImage: `url(${media.get('preview_url')})` }} onClick={this.handleOpen}>
{spoilerButton} {spoilerButton}
<div className='media-spoiler-video-play-icon'><i className='fa fa-play' /></div> <div className='media-spoiler-video-play-icon'><i className='fa fa-play' /></div>
</div> </button>
); );
} }

View File

@@ -23,6 +23,7 @@ const messages = defineMessages({
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' }, info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' },
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
}); });
const mapStateToProps = state => ({ const mapStateToProps = state => ({
@@ -66,15 +67,16 @@ export default class GettingStarted extends ImmutablePureComponent {
navItems = navItems.concat([ navItems = navItems.concat([
<ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />, <ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
<ColumnLink key='5' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
]); ]);
if (me.get('locked')) { if (me.get('locked')) {
navItems.push(<ColumnLink key='5' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />); navItems.push(<ColumnLink key='6' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
} }
navItems = navItems.concat([ navItems = navItems.concat([
<ColumnLink key='6' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />, <ColumnLink key='7' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
<ColumnLink key='7' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />, <ColumnLink key='8' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />,
]); ]);
return ( return (

View File

@@ -0,0 +1,59 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { fetchPinnedStatuses } from '../../actions/pin_statuses';
import Column from '../ui/components/column';
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import StatusList from '../../components/status_list';
import { defineMessages, injectIntl } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
const messages = defineMessages({
heading: { id: 'column.pins', defaultMessage: 'Pinned toot' },
});
const mapStateToProps = state => ({
statusIds: state.getIn(['status_lists', 'pins', 'items']),
hasMore: !!state.getIn(['status_lists', 'pins', 'next']),
});
@connect(mapStateToProps)
@injectIntl
export default class PinnedStatuses extends ImmutablePureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
statusIds: ImmutablePropTypes.list.isRequired,
intl: PropTypes.object.isRequired,
hasMore: PropTypes.bool.isRequired,
};
componentWillMount () {
this.props.dispatch(fetchPinnedStatuses());
}
handleHeaderClick = () => {
this.column.scrollTop();
}
setRef = c => {
this.column = c;
}
render () {
const { intl, statusIds, hasMore } = this.props;
return (
<Column icon='thumb-tack' heading={intl.formatMessage(messages.heading)} ref={this.setRef}>
<ColumnBackButtonSlim />
<StatusList
statusIds={statusIds}
scrollKey='pinned_statuses'
hasMore={hasMore}
/>
</Column>
);
}
}

View File

@@ -33,7 +33,8 @@ export default class EmbedModal extends ImmutablePureComponent {
iframeDocument.close(); iframeDocument.close();
iframeDocument.body.style.margin = 0; iframeDocument.body.style.margin = 0;
this.iframe.height = iframeDocument.body.scrollHeight + 'px'; this.iframe.width = iframeDocument.body.scrollWidth;
this.iframe.height = iframeDocument.body.scrollHeight;
}); });
} }
@@ -71,7 +72,6 @@ export default class EmbedModal extends ImmutablePureComponent {
<iframe <iframe
className='embed-modal__iframe' className='embed-modal__iframe'
scrolling='no'
frameBorder='0' frameBorder='0'
ref={this.setIframeRef} ref={this.setIframeRef}
title='preview' title='preview'

View File

@@ -35,6 +35,7 @@ import {
FavouritedStatuses, FavouritedStatuses,
Blocks, Blocks,
Mutes, Mutes,
PinnedStatuses,
} from './util/async-components'; } from './util/async-components';
// Dummy import, to make sure that <Status /> ends up in the application bundle. // Dummy import, to make sure that <Status /> ends up in the application bundle.
@@ -208,6 +209,7 @@ export default class UI extends React.PureComponent {
<WrappedRoute path='/notifications' component={Notifications} content={children} /> <WrappedRoute path='/notifications' component={Notifications} content={children} />
<WrappedRoute path='/favourites' component={FavouritedStatuses} 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/new' component={Compose} content={children} />
<WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} /> <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />

View File

@@ -34,6 +34,10 @@ export function GettingStarted () {
return import(/* webpackChunkName: "features/getting_started" */'../../getting_started'); return import(/* webpackChunkName: "features/getting_started" */'../../getting_started');
} }
export function PinnedStatuses () {
return import(/* webpackChunkName: "features/pinned_statuses" */'../../pinned_statuses');
}
export function AccountTimeline () { export function AccountTimeline () {
return import(/* webpackChunkName: "features/account_timeline" */'../../account_timeline'); return import(/* webpackChunkName: "features/account_timeline" */'../../account_timeline');
} }

View File

@@ -34,6 +34,7 @@
"column.mutes": "Muted users", "column.mutes": "Muted users",
"column.notifications": "Notifications", "column.notifications": "Notifications",
"column.public": "Federated timeline", "column.public": "Federated timeline",
"column.pins": "Pinned toots",
"column_back_button.label": "Back", "column_back_button.label": "Back",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left", "column_header.moveLeft_settings": "Move column to the left",
@@ -111,6 +112,7 @@
"navigation_bar.mutes": "Muted users", "navigation_bar.mutes": "Muted users",
"navigation_bar.preferences": "Preferences", "navigation_bar.preferences": "Preferences",
"navigation_bar.public_timeline": "Federated timeline", "navigation_bar.public_timeline": "Federated timeline",
"navigation_bar.pins": "Pinned toots",
"notification.favourite": "{name} favourited your status", "notification.favourite": "{name} favourited your status",
"notification.follow": "{name} followed you", "notification.follow": "{name} followed you",
"notification.mention": "{name} mentioned you", "notification.mention": "{name} mentioned you",

View File

@@ -34,6 +34,7 @@
"column.mutes": "Comptes masqués", "column.mutes": "Comptes masqués",
"column.notifications": "Notifications", "column.notifications": "Notifications",
"column.public": "Fil public global", "column.public": "Fil public global",
"column.pins": "Pouets épinglés",
"column_back_button.label": "Retour", "column_back_button.label": "Retour",
"column_header.hide_settings": "Masquer les paramètres", "column_header.hide_settings": "Masquer les paramètres",
"column_header.moveLeft_settings": "Déplacer la colonne vers la gauche", "column_header.moveLeft_settings": "Déplacer la colonne vers la gauche",
@@ -62,9 +63,9 @@
"confirmations.mute.confirm": "Masquer", "confirmations.mute.confirm": "Masquer",
"confirmations.mute.message": "Confirmez vous le masquage de {name}?", "confirmations.mute.message": "Confirmez vous le masquage de {name}?",
"confirmations.unfollow.confirm": "Ne plus suivre", "confirmations.unfollow.confirm": "Ne plus suivre",
"confirmations.unfollow.message": "Vous voulez-vous arrêter de suivre {name}?", "confirmations.unfollow.message": "Voulez-vous arrêter de suivre {name}?",
"embed.instructions": "Embed this status on your website by copying the code below.", "embed.instructions": "Intégrez ce statut à votre site en copiant ce code ci-dessous.",
"embed.preview": "Here is what it will look like:", "embed.preview": "Il apparaîtra comme cela:",
"emoji_button.activity": "Activités", "emoji_button.activity": "Activités",
"emoji_button.flags": "Drapeaux", "emoji_button.flags": "Drapeaux",
"emoji_button.food": "Boire et manger", "emoji_button.food": "Boire et manger",
@@ -111,6 +112,7 @@
"navigation_bar.mutes": "Comptes masqués", "navigation_bar.mutes": "Comptes masqués",
"navigation_bar.preferences": "Préférences", "navigation_bar.preferences": "Préférences",
"navigation_bar.public_timeline": "Fil public global", "navigation_bar.public_timeline": "Fil public global",
"navigation_bar.pins": "Pouets épinglés",
"notification.favourite": "{name} a ajouté à ses favoris:", "notification.favourite": "{name} a ajouté à ses favoris:",
"notification.follow": "{name} vous suit.", "notification.follow": "{name} vous suit.",
"notification.mention": "{name} vous a mentionné⋅e:", "notification.mention": "{name} vous a mentionné⋅e:",

View File

@@ -34,6 +34,7 @@
"column.mutes": "ミュートしたユーザー", "column.mutes": "ミュートしたユーザー",
"column.notifications": "通知", "column.notifications": "通知",
"column.public": "連合タイムライン", "column.public": "連合タイムライン",
"column.pins": "固定されたトゥート",
"column_back_button.label": "戻る", "column_back_button.label": "戻る",
"column_header.hide_settings": "設定を隠す", "column_header.hide_settings": "設定を隠す",
"column_header.moveLeft_settings": "カラムを左に移動する", "column_header.moveLeft_settings": "カラムを左に移動する",
@@ -111,6 +112,7 @@
"navigation_bar.mutes": "ミュートしたユーザー", "navigation_bar.mutes": "ミュートしたユーザー",
"navigation_bar.preferences": "ユーザー設定", "navigation_bar.preferences": "ユーザー設定",
"navigation_bar.public_timeline": "連合タイムライン", "navigation_bar.public_timeline": "連合タイムライン",
"navigation_bar.pins": "固定されたトゥート",
"notification.favourite": "{name}さんがあなたのトゥートをお気に入りに登録しました", "notification.favourite": "{name}さんがあなたのトゥートをお気に入りに登録しました",
"notification.follow": "{name}さんにフォローされました", "notification.follow": "{name}さんにフォローされました",
"notification.mention": "{name}さんがあなたに返信しました", "notification.mention": "{name}さんがあなたに返信しました",

View File

@@ -34,6 +34,7 @@
"column.mutes": "뮤트 중인 사용자", "column.mutes": "뮤트 중인 사용자",
"column.notifications": "알림", "column.notifications": "알림",
"column.public": "연합 타임라인", "column.public": "연합 타임라인",
"column.pins": "고정된 Toot",
"column_back_button.label": "돌아가기", "column_back_button.label": "돌아가기",
"column_header.hide_settings": "Hide settings", "column_header.hide_settings": "Hide settings",
"column_header.moveLeft_settings": "Move column to the left", "column_header.moveLeft_settings": "Move column to the left",
@@ -111,6 +112,7 @@
"navigation_bar.mutes": "뮤트 중인 사용자", "navigation_bar.mutes": "뮤트 중인 사용자",
"navigation_bar.preferences": "사용자 설정", "navigation_bar.preferences": "사용자 설정",
"navigation_bar.public_timeline": "연합 타임라인", "navigation_bar.public_timeline": "연합 타임라인",
"navigation_bar.pins": "고정된 Toot",
"notification.favourite": "{name}님이 즐겨찾기 했습니다", "notification.favourite": "{name}님이 즐겨찾기 했습니다",
"notification.follow": "{name}님이 나를 팔로우 했습니다", "notification.follow": "{name}님이 나를 팔로우 했습니다",
"notification.mention": "{name}님이 답글을 보냈습니다", "notification.mention": "{name}님이 답글을 보냈습니다",

View File

@@ -34,6 +34,7 @@
"column.mutes": "Personas en silenci", "column.mutes": "Personas en silenci",
"column.notifications": "Notificacions", "column.notifications": "Notificacions",
"column.public": "Flux public global", "column.public": "Flux public global",
"column.pins": "Tuts penjats",
"column_back_button.label": "Tornar", "column_back_button.label": "Tornar",
"column_header.hide_settings": "Amagar los paramètres", "column_header.hide_settings": "Amagar los paramètres",
"column_header.moveLeft_settings": "Desplaçar la colomna a man drecha", "column_header.moveLeft_settings": "Desplaçar la colomna a man drecha",
@@ -87,7 +88,7 @@
"getting_started.appsshort": "Apps", "getting_started.appsshort": "Apps",
"getting_started.faq": "FAQ", "getting_started.faq": "FAQ",
"getting_started.heading": "Per començar", "getting_started.heading": "Per començar",
"getting_started.open_source_notice": "Mastodon es un logicial liure. Podètz contribuir e mandar vòstres comentaris e rapòrt de bug via{github} sus GitHub.", "getting_started.open_source_notice": "Mastodon es un logicial liure. Podètz contribuir e mandar vòstres comentaris e rapòrt de bug via {github} sus GitHub.",
"getting_started.userguide": "Guida dutilizacion", "getting_started.userguide": "Guida dutilizacion",
"home.column_settings.advanced": "Avançat", "home.column_settings.advanced": "Avançat",
"home.column_settings.basic": "Basic", "home.column_settings.basic": "Basic",
@@ -111,6 +112,7 @@
"navigation_bar.mutes": "Personas rescondudas", "navigation_bar.mutes": "Personas rescondudas",
"navigation_bar.preferences": "Preferéncias", "navigation_bar.preferences": "Preferéncias",
"navigation_bar.public_timeline": "Flux public global", "navigation_bar.public_timeline": "Flux public global",
"navigation_bar.pins": "Tuts penjats",
"notification.favourite": "{name} a ajustat a sos favorits:", "notification.favourite": "{name} a ajustat a sos favorits:",
"notification.follow": "{name} vos sèc", "notification.follow": "{name} vos sèc",
"notification.mention": "{name} vos a mencionat:", "notification.mention": "{name} vos a mencionat:",
@@ -126,21 +128,21 @@
"notifications.column_settings.reblog": "Partatges:", "notifications.column_settings.reblog": "Partatges:",
"notifications.column_settings.show": "Mostrar dins la colomna", "notifications.column_settings.show": "Mostrar dins la colomna",
"notifications.column_settings.sound": "Emetre un son", "notifications.column_settings.sound": "Emetre un son",
"onboarding.done": "Fach", "onboarding.done": "Sortir",
"onboarding.next": "Seguent", "onboarding.next": "Seguent",
"onboarding.page_five.public_timelines": "Lo flux local mòstra los estatuts publics del monde de vòstra instància, aquí {domain}. Lo flux federat mòstra los estatuts publics de tot lo mond sus {domain} sègon. Son los fluxes publics, un bon biais de trobar de mond.", "onboarding.page_five.public_timelines": "Lo flux local mòstra los estatuts publics del monde de vòstra instància, aquí {domain}. Lo flux federat mòstra los estatuts publics de la gent que los de {domain} sègon. Son los fluxes publics, un bon biais de trobar de mond.",
"onboarding.page_four.home": "Lo flux dacuèlh mòstra los estatuts del mond que seguètz.", "onboarding.page_four.home": "Lo flux dacuèlh mòstra los estatuts del mond que seguètz.",
"onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualquun interagís amb vos", "onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualquun interagís amb vos",
"onboarding.page_one.federation": "Mastodon es un malhum de servidors independents que comunican per bastir un malhum ma larg. Òm los apèla instàncias.", "onboarding.page_one.federation": "Mastodon es un malhum de servidors independents que comunican per bastir un malhum mai larg. Òm los apèla instàncias.",
"onboarding.page_one.handle": "Sètz sus {domain}, doncas vòstre identificant complet es {handle}", "onboarding.page_one.handle": "Sètz sus {domain}, doncas vòstre identificant complet es {handle}",
"onboarding.page_one.welcome": "Benvengut a Mastodon!", "onboarding.page_one.welcome": "Benvengut a Mastodon!",
"onboarding.page_six.admin": "Vòstre administrator dinstància es {admin}.", "onboarding.page_six.admin": "Vòstre administrator dinstància es {admin}.",
"onboarding.page_six.almost_done": "Gaireben acabat…", "onboarding.page_six.almost_done": "Gaireben acabat…",
"onboarding.page_six.appetoot": "Bon Appetut!", "onboarding.page_six.appetoot": "Bon Appetut!",
"onboarding.page_six.apps_available": "I a daplicacions per mobil per iOS, Android e mai.", "onboarding.page_six.apps_available": "I a daplicacions per mobil per iOS, Android e mai.",
"onboarding.page_six.github": "Mastodon es un logicial liure e open-source. Podètz senhalar de bugs, demandar de foncionalitats e contribuir al còdi sus {github}.", "onboarding.page_six.github": "Mastodon es un logicial liure e open-source. Podètz senhalar de bugs, demandar de foncionalitats e contribuir al còdi sus {github}.",
"onboarding.page_six.guidelines": "guida de la comunitat", "onboarding.page_six.guidelines": "guida de la comunitat",
"onboarding.page_six.read_guidelines": "Mercés de legir la {guidelines} a {domain}!", "onboarding.page_six.read_guidelines": "Mercés de legir la {guidelines} de {domain}!",
"onboarding.page_six.various_app": "aplicacions per mobil", "onboarding.page_six.various_app": "aplicacions per mobil",
"onboarding.page_three.profile": "Modificatz vòstre perfil per cambiar vòstre avatar, bio e escais-nom. I a enlà totas las preferéncias.", "onboarding.page_three.profile": "Modificatz vòstre perfil per cambiar vòstre avatar, bio e escais-nom. I a enlà totas las preferéncias.",
"onboarding.page_three.search": "Emplegatz la barra de recèrca per trobar de mond e engachatz las etiquetas coma {illustration} e {introductions}. Per trobar una persona duna autra instància, picatz son identificant complet.", "onboarding.page_three.search": "Emplegatz la barra de recèrca per trobar de mond e engachatz las etiquetas coma {illustration} e {introductions}. Per trobar una persona duna autra instància, picatz son identificant complet.",
@@ -183,7 +185,7 @@
"status.show_less": "Tornar plegar", "status.show_less": "Tornar plegar",
"status.show_more": "Desplegar", "status.show_more": "Desplegar",
"status.unmute_conversation": "Conversacions amb silenci levat", "status.unmute_conversation": "Conversacions amb silenci levat",
"status.unpin": "Despenjar del perfil", "status.unpin": "Tirar del perfil",
"tabs_bar.compose": "Compausar", "tabs_bar.compose": "Compausar",
"tabs_bar.federated_timeline": "Flux public global", "tabs_bar.federated_timeline": "Flux public global",
"tabs_bar.home": "Acuèlh", "tabs_bar.home": "Acuèlh",

View File

@@ -12,7 +12,7 @@
"account.mute": "Wycisz @{name}", "account.mute": "Wycisz @{name}",
"account.posts": "Wpisy", "account.posts": "Wpisy",
"account.report": "Zgłoś @{name}", "account.report": "Zgłoś @{name}",
"account.requested": "Oczekująca prośba", "account.requested": "Oczekująca prośba, kliknij aby anulować",
"account.share": "Udostępnij profil @{name}", "account.share": "Udostępnij profil @{name}",
"account.unblock": "Odblokuj @{name}", "account.unblock": "Odblokuj @{name}",
"account.unblock_domain": "Odblokuj domenę {domain}", "account.unblock_domain": "Odblokuj domenę {domain}",
@@ -33,6 +33,7 @@
"column.home": "Strona główna", "column.home": "Strona główna",
"column.mutes": "Wyciszeni użytkownicy", "column.mutes": "Wyciszeni użytkownicy",
"column.notifications": "Powiadomienia", "column.notifications": "Powiadomienia",
"column.pins": "Przypięte wpisy",
"column.public": "Globalna oś czasu", "column.public": "Globalna oś czasu",
"column_back_button.label": "Wróć", "column_back_button.label": "Wróć",
"column_header.hide_settings": "Ukryj ustawienia", "column_header.hide_settings": "Ukryj ustawienia",
@@ -109,6 +110,7 @@
"navigation_bar.info": "Szczegółowe informacje", "navigation_bar.info": "Szczegółowe informacje",
"navigation_bar.logout": "Wyloguj", "navigation_bar.logout": "Wyloguj",
"navigation_bar.mutes": "Wyciszeni użytkownicy", "navigation_bar.mutes": "Wyciszeni użytkownicy",
"navigation_bar.pins": "Przypięte wpisy",
"navigation_bar.preferences": "Preferencje", "navigation_bar.preferences": "Preferencje",
"navigation_bar.public_timeline": "Oś czasu federacji", "navigation_bar.public_timeline": "Oś czasu federacji",
"notification.favourite": "{name} dodał Twój status do ulubionych", "notification.favourite": "{name} dodał Twój status do ulubionych",
@@ -153,8 +155,8 @@
"privacy.private.short": "Tylko dla śledzących", "privacy.private.short": "Tylko dla śledzących",
"privacy.public.long": "Widoczny na publicznych osiach czasu", "privacy.public.long": "Widoczny na publicznych osiach czasu",
"privacy.public.short": "Publiczny", "privacy.public.short": "Publiczny",
"privacy.unlisted.long": "Niewidoczne na publicznych osiach czasu", "privacy.unlisted.long": "Niewidoczny na publicznych osiach czasu",
"privacy.unlisted.short": "Niewidoczne", "privacy.unlisted.short": "Niewidoczny",
"reply_indicator.cancel": "Anuluj", "reply_indicator.cancel": "Anuluj",
"report.placeholder": "Dodatkowe komentarze", "report.placeholder": "Dodatkowe komentarze",
"report.submit": "Wyślij", "report.submit": "Wyślij",

View File

@@ -28,7 +28,7 @@ export default function reports(state = initialState, action) {
if (state.getIn(['new', 'account_id']) !== action.account.get('id')) { if (state.getIn(['new', 'account_id']) !== action.account.get('id')) {
map.setIn(['new', 'status_ids'], action.status ? ImmutableSet([action.status.getIn(['reblog', 'id'], action.status.get('id'))]) : ImmutableSet()); map.setIn(['new', 'status_ids'], action.status ? ImmutableSet([action.status.getIn(['reblog', 'id'], action.status.get('id'))]) : ImmutableSet());
map.setIn(['new', 'comment'], ''); map.setIn(['new', 'comment'], '');
} else { } else if (action.status) {
map.updateIn(['new', 'status_ids'], ImmutableSet(), set => set.add(action.status.getIn(['reblog', 'id'], action.status.get('id')))); map.updateIn(['new', 'status_ids'], ImmutableSet(), set => set.add(action.status.getIn(['reblog', 'id'], action.status.get('id'))));
} }
}); });

View File

@@ -2,10 +2,15 @@ import {
FAVOURITED_STATUSES_FETCH_SUCCESS, FAVOURITED_STATUSES_FETCH_SUCCESS,
FAVOURITED_STATUSES_EXPAND_SUCCESS, FAVOURITED_STATUSES_EXPAND_SUCCESS,
} from '../actions/favourites'; } from '../actions/favourites';
import {
PINNED_STATUSES_FETCH_SUCCESS,
} from '../actions/pin_statuses';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { import {
FAVOURITE_SUCCESS, FAVOURITE_SUCCESS,
UNFAVOURITE_SUCCESS, UNFAVOURITE_SUCCESS,
PIN_SUCCESS,
UNPIN_SUCCESS,
} from '../actions/interactions'; } from '../actions/interactions';
const initialState = ImmutableMap({ const initialState = ImmutableMap({
@@ -14,6 +19,11 @@ const initialState = ImmutableMap({
loaded: false, loaded: false,
items: ImmutableList(), items: ImmutableList(),
}), }),
pins: ImmutableMap({
next: null,
loaded: false,
items: ImmutableList(),
}),
}); });
const normalizeList = (state, listType, statuses, next) => { const normalizeList = (state, listType, statuses, next) => {
@@ -53,6 +63,12 @@ export default function statusLists(state = initialState, action) {
return prependOneToList(state, 'favourites', action.status); return prependOneToList(state, 'favourites', action.status);
case UNFAVOURITE_SUCCESS: case UNFAVOURITE_SUCCESS:
return removeOneFromList(state, 'favourites', action.status); return removeOneFromList(state, 'favourites', action.status);
case PINNED_STATUSES_FETCH_SUCCESS:
return normalizeList(state, 'pins', action.statuses, action.next);
case PIN_SUCCESS:
return prependOneToList(state, 'pins', action.status);
case UNPIN_SUCCESS:
return removeOneFromList(state, 'pins', action.status);
default: default:
return state; return state;
} }

View File

@@ -36,6 +36,9 @@ import {
FAVOURITED_STATUSES_FETCH_SUCCESS, FAVOURITED_STATUSES_FETCH_SUCCESS,
FAVOURITED_STATUSES_EXPAND_SUCCESS, FAVOURITED_STATUSES_EXPAND_SUCCESS,
} from '../actions/favourites'; } from '../actions/favourites';
import {
PINNED_STATUSES_FETCH_SUCCESS,
} from '../actions/pin_statuses';
import { SEARCH_FETCH_SUCCESS } from '../actions/search'; import { SEARCH_FETCH_SUCCESS } from '../actions/search';
import emojify from '../emoji'; import emojify from '../emoji';
import { Map as ImmutableMap, fromJS } from 'immutable'; import { Map as ImmutableMap, fromJS } from 'immutable';
@@ -138,6 +141,7 @@ export default function statuses(state = initialState, action) {
case NOTIFICATIONS_EXPAND_SUCCESS: case NOTIFICATIONS_EXPAND_SUCCESS:
case FAVOURITED_STATUSES_FETCH_SUCCESS: case FAVOURITED_STATUSES_FETCH_SUCCESS:
case FAVOURITED_STATUSES_EXPAND_SUCCESS: case FAVOURITED_STATUSES_EXPAND_SUCCESS:
case PINNED_STATUSES_FETCH_SUCCESS:
case SEARCH_FETCH_SUCCESS: case SEARCH_FETCH_SUCCESS:
return normalizeStatuses(state, action.statuses); return normalizeStatuses(state, action.statuses);
case TIMELINE_DELETE: case TIMELINE_DELETE:

View File

@@ -1,4 +1,21 @@
import loadPolyfills from '../mastodon/load_polyfills'; import loadPolyfills from '../mastodon/load_polyfills';
import ready from '../mastodon/ready';
window.addEventListener('message', e => {
const data = e.data || {};
if (!window.parent || data.type !== 'setHeight') {
return;
}
ready(() => {
window.parent.postMessage({
type: 'setHeight',
id: data.id,
height: document.getElementsByTagName('html')[0].scrollHeight,
}, '*');
});
});
function main() { function main() {
const { length } = require('stringz'); const { length } = require('stringz');
@@ -6,13 +23,13 @@ function main() {
const { delegate } = require('rails-ujs'); const { delegate } = require('rails-ujs');
const emojify = require('../mastodon/emoji').default; const emojify = require('../mastodon/emoji').default;
const { getLocale } = require('../mastodon/locales'); const { getLocale } = require('../mastodon/locales');
const ready = require('../mastodon/ready').default;
const { localeData } = getLocale(); const { localeData } = getLocale();
localeData.forEach(IntlRelativeFormat.__addLocaleData); localeData.forEach(IntlRelativeFormat.__addLocaleData);
ready(() => { ready(() => {
const locale = document.documentElement.lang; const locale = document.documentElement.lang;
const dateTimeFormat = new Intl.DateTimeFormat(locale, { const dateTimeFormat = new Intl.DateTimeFormat(locale, {
year: 'numeric', year: 'numeric',
month: 'long', month: 'long',
@@ -20,6 +37,7 @@ function main() {
hour: 'numeric', hour: 'numeric',
minute: 'numeric', minute: 'numeric',
}); });
const relativeFormat = new IntlRelativeFormat(locale); const relativeFormat = new IntlRelativeFormat(locale);
[].forEach.call(document.querySelectorAll('.emojify'), (content) => { [].forEach.call(document.querySelectorAll('.emojify'), (content) => {
@@ -29,12 +47,14 @@ function main() {
[].forEach.call(document.querySelectorAll('time.formatted'), (content) => { [].forEach.call(document.querySelectorAll('time.formatted'), (content) => {
const datetime = new Date(content.getAttribute('datetime')); const datetime = new Date(content.getAttribute('datetime'));
const formattedDate = dateTimeFormat.format(datetime); const formattedDate = dateTimeFormat.format(datetime);
content.title = formattedDate; content.title = formattedDate;
content.textContent = formattedDate; content.textContent = formattedDate;
}); });
[].forEach.call(document.querySelectorAll('time.time-ago'), (content) => { [].forEach.call(document.querySelectorAll('time.time-ago'), (content) => {
const datetime = new Date(content.getAttribute('datetime')); const datetime = new Date(content.getAttribute('datetime'));
content.title = dateTimeFormat.format(datetime); content.title = dateTimeFormat.format(datetime);
content.textContent = relativeFormat.format(datetime); content.textContent = relativeFormat.format(datetime);
}); });
@@ -45,10 +65,6 @@ function main() {
window.open(e.target.href, 'mastodon-intent', 'width=400,height=400,resizable=no,menubar=no,status=no,scrollbars=yes'); window.open(e.target.href, 'mastodon-intent', 'width=400,height=400,resizable=no,menubar=no,status=no,scrollbars=yes');
}); });
}); });
if (window.parent) {
window.parent.postMessage(['setHeight', document.getElementsByTagName('html')[0].scrollHeight], '*');
}
}); });
delegate(document, '.video-player video', 'click', ({ target }) => { delegate(document, '.video-player video', 'click', ({ target }) => {
@@ -77,6 +93,7 @@ function main() {
delegate(document, '.status__content__spoiler-link', 'click', ({ target }) => { delegate(document, '.status__content__spoiler-link', 'click', ({ target }) => {
const contentEl = target.parentNode.parentNode.querySelector('.e-content'); const contentEl = target.parentNode.parentNode.querySelector('.e-content');
if (contentEl.style.display === 'block') { if (contentEl.style.display === 'block') {
contentEl.style.display = 'none'; contentEl.style.display = 'none';
target.parentNode.style.marginBottom = 0; target.parentNode.style.marginBottom = 0;
@@ -84,11 +101,13 @@ function main() {
contentEl.style.display = 'block'; contentEl.style.display = 'block';
target.parentNode.style.marginBottom = null; target.parentNode.style.marginBottom = null;
} }
return false; return false;
}); });
delegate(document, '.account_display_name', 'input', ({ target }) => { delegate(document, '.account_display_name', 'input', ({ target }) => {
const nameCounter = document.querySelector('.name-counter'); const nameCounter = document.querySelector('.name-counter');
if (nameCounter) { if (nameCounter) {
nameCounter.textContent = 30 - length(target.value); nameCounter.textContent = 30 - length(target.value);
} }
@@ -96,6 +115,7 @@ function main() {
delegate(document, '.account_note', 'input', ({ target }) => { delegate(document, '.account_note', 'input', ({ target }) => {
const noteCounter = document.querySelector('.note-counter'); const noteCounter = document.querySelector('.note-counter');
if (noteCounter) { if (noteCounter) {
noteCounter.textContent = 160 - length(target.value); noteCounter.textContent = 160 - length(target.value);
} }
@@ -105,6 +125,7 @@ function main() {
const avatar = document.querySelector('.card.compact .avatar img'); const avatar = document.querySelector('.card.compact .avatar img');
const [file] = target.files || []; const [file] = target.files || [];
const url = URL.createObjectURL(file); const url = URL.createObjectURL(file);
avatar.src = url; avatar.src = url;
}); });
@@ -112,6 +133,7 @@ function main() {
const header = document.querySelector('.card.compact'); const header = document.querySelector('.card.compact');
const [file] = target.files || []; const [file] = target.files || [];
const url = URL.createObjectURL(file); const url = URL.createObjectURL(file);
header.style.backgroundImage = `url(${url})`; header.style.backgroundImage = `url(${url})`;
}); });
} }

View File

@@ -190,11 +190,15 @@
.filters { .filters {
display: flex; display: flex;
margin-bottom: 20px; flex-wrap: wrap;
.filter-subset { .filter-subset {
flex: 0 0 auto; flex: 0 0 auto;
margin-right: 40px; margin: 0 40px 10px 0;
&:last-child {
margin-bottom: 20px;
}
ul { ul {
margin-top: 5px; margin-top: 5px;

View File

@@ -45,6 +45,7 @@ body {
&.embed { &.embed {
background: transparent; background: transparent;
margin: 0; margin: 0;
padding-bottom: 0;
.container { .container {
position: absolute; position: absolute;

View File

@@ -2347,22 +2347,6 @@ button.icon-button.active i.fa-retweet {
height: 100%; height: 100%;
} }
.media-spoiler__video {
align-items: center;
background: $base-overlay-background;
color: $primary-text-color;
cursor: pointer;
display: flex;
flex-direction: column;
border: 0;
width: 100%;
height: 100%;
justify-content: center;
position: relative;
text-align: center;
z-index: 100;
}
.media-spoiler__warning { .media-spoiler__warning {
display: block; display: block;
font-size: 14px; font-size: 14px;
@@ -3821,6 +3805,8 @@ button.icon-button.active i.fa-retweet {
cursor: pointer; cursor: pointer;
margin-top: 8px; margin-top: 8px;
position: relative; position: relative;
border: 0;
display: block;
} }
.media-spoiler-video-play-icon { .media-spoiler-video-play-icon {

View File

@@ -3,7 +3,6 @@
max-width: 100%; max-width: 100%;
border-spacing: 0; border-spacing: 0;
border-collapse: collapse; border-collapse: collapse;
margin-bottom: 20px;
th, th,
td { td {
@@ -43,19 +42,17 @@
font-weight: 500; font-weight: 500;
} }
&.inline-table { &.inline-table > tbody > tr:nth-child(odd) > td,
td, &.inline-table > tbody > tr:nth-child(odd) > th {
th { background: transparent;
padding: 8px 2px;
}
& > tbody > tr:nth-child(odd) > td,
& > tbody > tr:nth-child(odd) > th {
background: transparent;
}
} }
} }
.table-wrapper {
overflow: auto;
margin-bottom: 20px;
}
samp { samp {
font-family: 'mastodon-font-monospace', monospace; font-family: 'mastodon-font-monospace', monospace;
} }

View File

@@ -96,12 +96,14 @@ class ActivityPub::TagManager
when 'Account' when 'Account'
klass.find_local(uri_to_local_id(uri, :username)) klass.find_local(uri_to_local_id(uri, :username))
else else
klass.find_by(id: uri_to_local_id(uri)) StatusFinder.new(uri).status
end end
elsif ::TagManager.instance.local_id?(uri) elsif ::TagManager.instance.local_id?(uri)
klass.find_by(id: ::TagManager.instance.unique_tag_to_local_id(uri, klass.to_s)) klass.find_by(id: ::TagManager.instance.unique_tag_to_local_id(uri, klass.to_s))
else else
klass.find_by(uri: uri.split('#').first) klass.find_by(uri: uri.split('#').first)
end end
rescue ActiveRecord::RecordNotFound
nil
end end
end end

View File

@@ -20,7 +20,16 @@ class LanguageDetector
private private
def detected_language_code def detected_language_code
result.language.to_sym if detected_language_reliable? iso6391(result.language).to_sym if detected_language_reliable?
end
def iso6391(bcp47)
iso639 = bcp47.split('-').first
# CLD3 returns grandfathered language code for Hebrew
return 'he' if iso639 == 'iw'
ISO_639.find(iso639).alpha2
end end
def result def result

View File

@@ -22,6 +22,8 @@ class Report < ApplicationRecord
scope :unresolved, -> { where(action_taken: false) } scope :unresolved, -> { where(action_taken: false) }
scope :resolved, -> { where(action_taken: true) } scope :resolved, -> { where(action_taken: true) }
validates :comment, length: { maximum: 1000 }
def statuses def statuses
Status.where(id: status_ids).includes(:account, :media_attachments, :mentions) Status.where(id: status_ids).includes(:account, :media_attachments, :mentions)
end end

View File

@@ -22,7 +22,7 @@
# reblogs_count :integer default(0), not null # reblogs_count :integer default(0), not null
# language :string # language :string
# conversation_id :integer # conversation_id :integer
# local :boolean default(FALSE) # local :boolean
# #
class Status < ApplicationRecord class Status < ApplicationRecord
@@ -63,8 +63,8 @@ class Status < ApplicationRecord
default_scope { recent } default_scope { recent }
scope :recent, -> { reorder(id: :desc) } scope :recent, -> { reorder(id: :desc) }
scope :remote, -> { where.not(uri: nil) } scope :remote, -> { where(local: false).or(where.not(uri: nil)) }
scope :local, -> { where(uri: nil) } scope :local, -> { where(local: true).or(where(uri: nil)) }
scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') } scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') }
scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') } scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') }

View File

@@ -40,12 +40,12 @@ class OEmbedSerializer < ActiveModel::Serializer
attributes = { attributes = {
src: embed_short_account_status_url(object.account, object), src: embed_short_account_status_url(object.account, object),
class: 'mastodon-embed', class: 'mastodon-embed',
style: 'max-width: 100%; border: none;', style: 'max-width: 100%; border: 0',
width: width, width: width,
height: height, height: height,
} }
content_tag :iframe, nil, attributes content_tag(:iframe, nil, attributes) + content_tag(:script, nil, src: full_asset_url('embed.js'), async: true)
end end
def width def width

View File

@@ -8,11 +8,12 @@ class ActivityPub::ProcessAccountService < BaseService
def call(username, domain, json) def call(username, domain, json)
return if json['inbox'].blank? return if json['inbox'].blank?
@json = json @json = json
@uri = @json['id'] @uri = @json['id']
@username = username @username = username
@domain = domain @domain = domain
@account = Account.find_by(uri: @uri) @account = Account.find_by(uri: @uri)
@collections = {}
create_account if @account.nil? create_account if @account.nil?
upgrade_account if @account.ostatus? upgrade_account if @account.ostatus?
@@ -47,11 +48,14 @@ class ActivityPub::ProcessAccountService < BaseService
@account.url = url || @uri @account.url = url || @uri
@account.display_name = @json['name'] || '' @account.display_name = @json['name'] || ''
@account.note = @json['summary'] || '' @account.note = @json['summary'] || ''
@account.avatar_remote_url = image_url('icon') @account.avatar_remote_url = image_url('icon') unless skip_download?
@account.header_remote_url = image_url('image') @account.header_remote_url = image_url('image') unless skip_download?
@account.public_key = public_key || '' @account.public_key = public_key || ''
@account.locked = @json['manuallyApprovesFollowers'] || false @account.locked = @json['manuallyApprovesFollowers'] || false
@account.save! @account.statuses_count = outbox_total_items if outbox_total_items.present?
@account.following_count = following_total_items if following_total_items.present?
@account.followers_count = followers_total_items if followers_total_items.present?
@account.save_with_optional_media!
end end
def upgrade_account def upgrade_account
@@ -88,6 +92,33 @@ class ActivityPub::ProcessAccountService < BaseService
value['href'] value['href']
end end
def outbox_total_items
collection_total_items('outbox')
end
def following_total_items
collection_total_items('following')
end
def followers_total_items
collection_total_items('followers')
end
def collection_total_items(type)
return if @json[type].blank?
return @collections[type] if @collections.key?(type)
collection = fetch_resource(@json[type])
@collections[type] = collection.is_a?(Hash) && collection['totalItems'].present? && collection['totalItems'].is_a?(Numeric) ? collection['totalItems'] : nil
rescue HTTP::Error, OpenSSL::SSL::SSLError
@collections[type] = nil
end
def skip_download?
@account.suspended? || domain_block&.reject_media?
end
def auto_suspend? def auto_suspend?
domain_block && domain_block.suspend? domain_block && domain_block.suspend?
end end

View File

@@ -12,7 +12,7 @@ class FollowService < BaseService
raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended? raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended?
raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account) raise Mastodon::NotPermittedError if target_account.blocking?(source_account) || source_account.blocking?(target_account)
return if source_account.following?(target_account) return if source_account.following?(target_account) || source_account.requested?(target_account)
if target_account.locked? || target_account.activitypub? if target_account.locked? || target_account.activitypub?
request_follow(source_account, target_account) request_follow(source_account, target_account)

View File

@@ -27,9 +27,10 @@ class PostStatusService < BaseService
thread: in_reply_to, thread: in_reply_to,
sensitive: options[:sensitive], sensitive: options[:sensitive],
spoiler_text: options[:spoiler_text] || '', spoiler_text: options[:spoiler_text] || '',
visibility: options[:visibility], visibility: options[:visibility] || account.user&.setting_default_privacy,
language: detect_language_for(text, account), language: detect_language_for(text, account),
application: options[:application]) application: options[:application])
attach_media(status, media) attach_media(status, media)
end end

View File

@@ -4,16 +4,19 @@ class UnsubscribeService < BaseService
def call(account) def call(account)
return if account.hub_url.blank? return if account.hub_url.blank?
@account = account @account = account
@response = build_request.perform
Rails.logger.debug "PuSH unsubscribe for #{@account.acct} failed: #{@response.status}" unless @response.status.success? begin
@response = build_request.perform
Rails.logger.debug "PuSH unsubscribe for #{@account.acct} failed: #{@response.status}" unless @response.status.success?
rescue HTTP::Error, OpenSSL::SSL::SSLError => e
Rails.logger.debug "PuSH unsubscribe for #{@account.acct} failed: #{e}"
end
@account.secret = '' @account.secret = ''
@account.subscription_expires_at = nil @account.subscription_expires_at = nil
@account.save! @account.save!
rescue HTTP::Error, OpenSSL::SSL::SSLError
Rails.logger.debug "PuSH subscription request for #{@account.acct} could not be made due to HTTP or SSL error"
end end
private private

View File

@@ -1,16 +1,17 @@
%table.table .table-wrapper
%tbody %table.table
%tr %tbody
%td= t('admin.accounts.show.created_reports')
%td= link_to pluralize(account.reports.count, t('admin.accounts.show.report')), admin_reports_path(account_id: account.id)
%tr
%td= t('admin.accounts.show.targeted_reports')
%td= link_to pluralize(account.targeted_reports.count, t('admin.accounts.show.report')), admin_reports_path(target_account_id: account.id)
- if account.silenced? || account.suspended?
%tr %tr
%td= t('admin.accounts.moderation.title') %td= t('admin.accounts.show.created_reports')
%td %td= link_to pluralize(account.reports.count, t('admin.accounts.show.report')), admin_reports_path(account_id: account.id)
- if account.silenced? %tr
%p= t('admin.accounts.moderation.silenced') %td= t('admin.accounts.show.targeted_reports')
- if account.suspended? %td= link_to pluralize(account.targeted_reports.count, t('admin.accounts.show.report')), admin_reports_path(target_account_id: account.id)
%p= t('admin.accounts.moderation.suspended') - if account.silenced? || account.suspended?
%tr
%td= t('admin.accounts.moderation.title')
%td
- if account.silenced?
%p= t('admin.accounts.moderation.silenced')
- if account.suspended?
%p= t('admin.accounts.moderation.suspended')

View File

@@ -50,16 +50,17 @@
%button= t('admin.accounts.search') %button= t('admin.accounts.search')
= link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative' = link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative'
%table.table .table-wrapper
%thead %table.table
%tr %thead
%th= t('admin.accounts.username') %tr
%th= t('admin.accounts.domain') %th= t('admin.accounts.username')
%th= t('admin.accounts.protocol') %th= t('admin.accounts.domain')
%th= t('admin.accounts.confirmed') %th= t('admin.accounts.protocol')
%th= fa_icon 'paper-plane-o' %th= t('admin.accounts.confirmed')
%th %th= fa_icon 'paper-plane-o'
%tbody %th
= render @accounts %tbody
= render @accounts
= paginate @accounts = paginate @accounts

View File

@@ -1,86 +1,86 @@
- content_for :page_title do - content_for :page_title do
= @account.acct = @account.acct
%table.table .table-wrapper
%tbody %table.table
%tr %tbody
%th= t('admin.accounts.username') %tr
%td= @account.username %th= t('admin.accounts.username')
%tr %td= @account.username
%th= t('admin.accounts.domain') %tr
%td= @account.domain %th= t('admin.accounts.domain')
%tr %td= @account.domain
%th= t('admin.accounts.display_name') %tr
%td= @account.display_name %th= t('admin.accounts.display_name')
%td= @account.display_name
- if @account.local? - if @account.local?
%tr
%th= t('admin.accounts.email')
%td= @account.user_email
%tr
%th= t('admin.accounts.most_recent_ip')
%td= @account.user_current_sign_in_ip
%tr
%th= t('admin.accounts.most_recent_activity')
%td
- if @account.user_current_sign_in_at
%time.formatted{ datetime: @account.user_current_sign_in_at.iso8601, title: l(@account.user_current_sign_in_at) }
= l @account.user_current_sign_in_at
- else
Never
- else
%tr
%th= t('admin.accounts.profile_url')
%td= link_to @account.url, @account.url
%tr
%th= t('admin.accounts.protocol')
%td= @account.protocol.humanize
- if @account.ostatus?
%tr %tr
%th= t('admin.accounts.feed_url') %th= t('admin.accounts.email')
%td= link_to @account.remote_url, @account.remote_url %td= @account.user_email
%tr %tr
%th= t('admin.accounts.push_subscription_expires') %th= t('admin.accounts.most_recent_ip')
%td= @account.user_current_sign_in_ip
%tr
%th= t('admin.accounts.most_recent_activity')
%td %td
- if @account.subscribed? - if @account.user_current_sign_in_at
%time.formatted{ datetime: @account.subscription_expires_at.iso8601, title: l(@account.subscription_expires_at) } %time.formatted{ datetime: @account.user_current_sign_in_at.iso8601, title: l(@account.user_current_sign_in_at) }
= l @account.subscription_expires_at = l @account.user_current_sign_in_at
- else - else
= t('admin.accounts.not_subscribed') Never
- else
%tr %tr
%th= t('admin.accounts.salmon_url') %th= t('admin.accounts.profile_url')
%td= link_to @account.salmon_url, @account.salmon_url %td= link_to @account.url, @account.url
- elsif @account.activitypub?
%tr %tr
%th= t('admin.accounts.inbox_url') %th= t('admin.accounts.protocol')
%td= link_to @account.inbox_url, @account.inbox_url %td= @account.protocol.humanize
%tr
%th= t('admin.accounts.outbox_url')
%td= link_to @account.outbox_url, @account.outbox_url
%tr - if @account.ostatus?
%th= t('admin.accounts.follows') %tr
%td= @account.following_count %th= t('admin.accounts.feed_url')
%tr %td= link_to @account.remote_url, @account.remote_url
%th= t('admin.accounts.followers') %tr
%td= @account.followers_count %th= t('admin.accounts.push_subscription_expires')
%tr %td
%th= t('admin.accounts.statuses') - if @account.subscribed?
%td= link_to @account.statuses_count, admin_account_statuses_path(@account.id) %time.formatted{ datetime: @account.subscription_expires_at.iso8601, title: l(@account.subscription_expires_at) }
%tr = l @account.subscription_expires_at
%th= t('admin.accounts.media_attachments') - else
%td = t('admin.accounts.not_subscribed')
= link_to @account.media_attachments.count, admin_account_statuses_path(@account.id, { media: true }) %tr
= surround '(', ')' do %th= t('admin.accounts.salmon_url')
= number_to_human_size @account.media_attachments.sum('file_file_size') %td= link_to @account.salmon_url, @account.salmon_url
%tr - elsif @account.activitypub?
%th= t('.created_reports') %tr
%td= link_to pluralize(@account.reports.count, t('.report')), admin_reports_path(account_id: @account.id) %th= t('admin.accounts.inbox_url')
%tr %td= link_to @account.inbox_url, @account.inbox_url
%th= t('.targeted_reports') %tr
%td= link_to pluralize(@account.targeted_reports.count, t('.report')), admin_reports_path(target_account_id: @account.id) %th= t('admin.accounts.outbox_url')
%td= link_to @account.outbox_url, @account.outbox_url
%tr
%th= t('admin.accounts.follows')
%td= @account.following_count
%tr
%th= t('admin.accounts.followers')
%td= @account.followers_count
%tr
%th= t('admin.accounts.statuses')
%td= link_to @account.statuses_count, admin_account_statuses_path(@account.id)
%tr
%th= t('admin.accounts.media_attachments')
%td
= link_to @account.media_attachments.count, admin_account_statuses_path(@account.id, { media: true })
= surround '(', ')' do
= number_to_human_size @account.media_attachments.sum('file_file_size')
%tr
%th= t('.created_reports')
%td= link_to pluralize(@account.reports.count, t('.report')), admin_reports_path(account_id: @account.id)
%tr
%th= t('.targeted_reports')
%td= link_to pluralize(@account.targeted_reports.count, t('.report')), admin_reports_path(target_account_id: @account.id)
%div{ style: 'float: right' } %div{ style: 'float: right' }
- if @account.local? - if @account.local?

View File

@@ -1,15 +1,16 @@
- content_for :page_title do - content_for :page_title do
= t('admin.domain_blocks.title') = t('admin.domain_blocks.title')
%table.table .table-wrapper
%thead %table.table
%tr %thead
%th= t('admin.domain_blocks.domain') %tr
%th= t('admin.domain_blocks.severity') %th= t('admin.domain_blocks.domain')
%th= t('admin.domain_blocks.reject_media') %th= t('admin.domain_blocks.severity')
%th %th= t('admin.domain_blocks.reject_media')
%tbody %th
= render @domain_blocks %tbody
= render @domain_blocks
= paginate @domain_blocks = paginate @domain_blocks
= link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path, class: 'button' = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path, class: 'button'

View File

@@ -1,12 +1,13 @@
- content_for :page_title do - content_for :page_title do
= t('admin.instances.title') = t('admin.instances.title')
%table.table .table-wrapper
%thead %table.table
%tr %thead
%th= t('admin.instances.domain_name') %tr
%th= t('admin.instances.account_count') %th= t('admin.instances.domain_name')
%tbody %th= t('admin.instances.account_count')
= render @instances %tbody
= render @instances
= paginate paginated_instances = paginate paginated_instances

View File

@@ -10,17 +10,18 @@
= form_tag do = form_tag do
%table.table .table-wrapper
%thead %table.table
%tr %thead
-# %th %tr
%th= t('admin.reports.id') -# %th
%th= t('admin.reports.target') %th= t('admin.reports.id')
%th= t('admin.reports.reported_by') %th= t('admin.reports.target')
%th= t('admin.reports.comment.label') %th= t('admin.reports.reported_by')
%th= t('admin.reports.report_contents') %th= t('admin.reports.comment.label')
%th %th= t('admin.reports.report_contents')
%tbody %th
= render @reports %tbody
= render @reports
= paginate @reports = paginate @reports

View File

@@ -1,15 +1,16 @@
- content_for :page_title do - content_for :page_title do
= t('admin.subscriptions.title') = t('admin.subscriptions.title')
%table.table .table-wrapper
%thead %table.table
%tr %thead
%th= t('admin.subscriptions.topic') %tr
%th= t('admin.subscriptions.callback_url') %th= t('admin.subscriptions.topic')
%th= t('admin.subscriptions.confirmed') %th= t('admin.subscriptions.callback_url')
%th= t('admin.subscriptions.expires_in') %th= t('admin.subscriptions.confirmed')
%th= t('admin.subscriptions.last_delivery') %th= t('admin.subscriptions.expires_in')
%tbody %th= t('admin.subscriptions.last_delivery')
= render @subscriptions %tbody
= render @subscriptions
= paginate @subscriptions = paginate @subscriptions

View File

@@ -1,28 +1,29 @@
%h6= t 'sessions.title' %h6= t 'sessions.title'
%p.muted-hint= t 'sessions.explanation' %p.muted-hint= t 'sessions.explanation'
%table.table.inline-table .table-wrapper
%thead %table.table.inline-table
%tr %thead
%th= t 'sessions.browser'
%th= t 'sessions.ip'
%th= t 'sessions.activity'
%td
%tbody
- @sessions.each do |session|
%tr %tr
%th= t 'sessions.browser'
%th= t 'sessions.ip'
%th= t 'sessions.activity'
%td %td
%span{ title: session.user_agent }< %tbody
= fa_icon "#{session_device_icon(session)} fw", 'aria-label' => session_device_icon(session) - @sessions.each do |session|
= ' ' %tr
= t 'sessions.description', browser: t("sessions.browsers.#{session.browser}"), platform: t("sessions.platforms.#{session.platform}") %td
%td %span{ title: session.user_agent }<
%samp= session.ip = fa_icon "#{session_device_icon(session)} fw", 'aria-label' => session_device_icon(session)
%td = ' '
- if current_session.session_id == session.session_id = t 'sessions.description', browser: t("sessions.browsers.#{session.browser}"), platform: t("sessions.platforms.#{session.platform}")
= t 'sessions.current_session' %td
- else %samp= session.ip
%time.time-ago{ datetime: session.updated_at.iso8601, title: l(session.updated_at) }= l(session.updated_at) %td
%td - if current_session.session_id == session.session_id
- if current_session.session_id != session.session_id = t 'sessions.current_session'
= table_link_to 'times', t('sessions.revoke'), settings_session_path(session), method: :delete - else
%time.time-ago{ datetime: session.updated_at.iso8601, title: l(session.updated_at) }= l(session.updated_at)
%td
- if current_session.session_id != session.session_id
= table_link_to 'times', t('sessions.revoke'), settings_session_path(session), method: :delete

View File

@@ -1,23 +1,24 @@
- content_for :page_title do - content_for :page_title do
= t('doorkeeper.authorized_applications.index.title') = t('doorkeeper.authorized_applications.index.title')
%table.table .table-wrapper
%thead %table.table
%tr %thead
%th= t('doorkeeper.authorized_applications.index.application')
%th= t('doorkeeper.authorized_applications.index.scopes')
%th= t('doorkeeper.authorized_applications.index.created_at')
%th
%tbody
- @applications.each do |application|
%tr %tr
%td %th= t('doorkeeper.authorized_applications.index.application')
- if application.website.blank? %th= t('doorkeeper.authorized_applications.index.scopes')
= application.name %th= t('doorkeeper.authorized_applications.index.created_at')
- else %th
= link_to application.name, application.website, target: '_blank', rel: 'noopener' %tbody
%th!= application.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.join('<br />') - @applications.each do |application|
%td= l application.created_at %tr
%td %td
- unless application.superapp? - if application.website.blank?
= table_link_to 'times', t('doorkeeper.authorized_applications.buttons.revoke'), oauth_authorized_application_path(application), method: :delete, data: { confirm: t('doorkeeper.authorized_applications.confirmations.revoke') } = application.name
- else
= link_to application.name, application.website, target: '_blank', rel: 'noopener'
%th!= application.scopes.map { |scope| t(scope, scope: [:doorkeeper, :scopes]) }.join('<br />')
%td= l application.created_at
%td
- unless application.superapp?
= table_link_to 'times', t('doorkeeper.authorized_applications.buttons.revoke'), oauth_authorized_application_path(application), method: :delete, data: { confirm: t('doorkeeper.authorized_applications.confirmations.revoke') }

View File

@@ -1,19 +1,20 @@
- content_for :page_title do - content_for :page_title do
= t('doorkeeper.applications.index.title') = t('doorkeeper.applications.index.title')
%table.table .table-wrapper
%thead %table.table
%tr %thead
%th= t('doorkeeper.applications.index.application')
%th= t('doorkeeper.applications.index.scopes')
%th
%tbody
- @applications.each do |application|
%tr %tr
%td= link_to application.name, settings_application_path(application) %th= t('doorkeeper.applications.index.application')
%th= application.scopes %th= t('doorkeeper.applications.index.scopes')
%td %th
= table_link_to 'times', t('doorkeeper.applications.index.delete'), settings_application_path(application), method: :delete, data: { confirm: t('doorkeeper.applications.confirmations.destroy') } %tbody
- @applications.each do |application|
%tr
%td= link_to application.name, settings_application_path(application)
%th= application.scopes
%td
= table_link_to 'times', t('doorkeeper.applications.index.delete'), settings_application_path(application), method: :delete, data: { confirm: t('doorkeeper.applications.confirmations.destroy') }
= paginate @applications = paginate @applications
= link_to t('doorkeeper.applications.index.new'), new_settings_application_path, class: 'button' = link_to t('doorkeeper.applications.index.new'), new_settings_application_path, class: 'button'

View File

@@ -3,22 +3,23 @@
%p.hint= t('applications.warning') %p.hint= t('applications.warning')
%table.table .table-wrapper
%tbody %table.table
%tr %tbody
%th= t('doorkeeper.applications.show.application_id') %tr
%td %th= t('doorkeeper.applications.show.application_id')
%code= @application.uid %td
%tr %code= @application.uid
%th= t('doorkeeper.applications.show.secret') %tr
%td %th= t('doorkeeper.applications.show.secret')
%code= @application.secret %td
%tr %code= @application.secret
%th{ rowspan: 2}= t('applications.your_token') %tr
%td %th{ rowspan: 2}= t('applications.your_token')
%code= current_user.token_for_app(@application).token %td
%tr %code= current_user.token_for_app(@application).token
%td= table_link_to 'refresh', t('applications.regenerate_token'), regenerate_settings_application_path(@application), method: :post %tr
%td= table_link_to 'refresh', t('applications.regenerate_token'), regenerate_settings_application_path(@application), method: :post
%hr/ %hr/

View File

@@ -1,21 +1,22 @@
- content_for :page_title do - content_for :page_title do
= t('settings.export') = t('settings.export')
%table.table .table-wrapper
%tbody %table.table
%tr %tbody
%th= t('exports.storage') %tr
%td= number_to_human_size @export.total_storage %th= t('exports.storage')
%td %td= number_to_human_size @export.total_storage
%tr %td
%th= t('exports.follows') %tr
%td= @export.total_follows %th= t('exports.follows')
%td= table_link_to 'download', t('exports.csv'), settings_exports_follows_path(format: :csv) %td= @export.total_follows
%tr %td= table_link_to 'download', t('exports.csv'), settings_exports_follows_path(format: :csv)
%th= t('exports.blocks') %tr
%td= @export.total_blocks %th= t('exports.blocks')
%td= table_link_to 'download', t('exports.csv'), settings_exports_blocks_path(format: :csv) %td= @export.total_blocks
%tr %td= table_link_to 'download', t('exports.csv'), settings_exports_blocks_path(format: :csv)
%th= t('exports.mutes') %tr
%td= @export.total_mutes %th= t('exports.mutes')
%td= table_link_to 'download', t('exports.csv'), settings_exports_mutes_path(format: :csv) %td= @export.total_mutes
%td= table_link_to 'download', t('exports.csv'), settings_exports_mutes_path(format: :csv)

View File

@@ -12,20 +12,21 @@
%p= t('followers.explanation_html') %p= t('followers.explanation_html')
%p= t('followers.true_privacy_html') %p= t('followers.true_privacy_html')
%table.table .table-wrapper
%thead %table.table
%tr %thead
%th
%th= t('followers.domain')
%th= t('followers.followers_count')
%tbody
- @domains.each do |domain|
%tr %tr
%td %th
= check_box_tag 'select[]', domain.domain, false, disabled: !@account.locked? unless domain.domain.nil? %th= t('followers.domain')
%td %th= t('followers.followers_count')
%samp= domain.domain.presence || Rails.configuration.x.local_domain %tbody
%td= number_with_delimiter domain.accounts_from_domain - @domains.each do |domain|
%tr
%td
= check_box_tag 'select[]', domain.domain, false, disabled: !@account.locked? unless domain.domain.nil?
%td
%samp= domain.domain.presence || Rails.configuration.x.local_domain
%td= number_with_delimiter domain.accounts_from_domain
.action-pagination .action-pagination
.actions .actions

View File

@@ -13,7 +13,7 @@
selected: I18n.locale selected: I18n.locale
= f.input :filtered_languages, = f.input :filtered_languages,
collection: I18n.available_locales, collection: filterable_languages,
wrapper: :with_block_label, wrapper: :with_block_label,
include_blank: false, include_blank: false,
label_method: lambda { |locale| human_locale(locale) }, label_method: lambda { |locale| human_locale(locale) },

View File

@@ -1,8 +1,8 @@
<p>Bonjorn <%= @resource.email %>&nbsp;!<p> <p>Bonjorn <%= @resource.email %>!<p>
<p>Venètz de vos crear un compte sus <%= @instance %> e vos mercegem :)</p> <p>Venètz de vos crear un compte sus <%= @instance %> e vos mercegem :)</p>
<p>Per confirmar vòstre inscripcion, mercés de clicar sul ligam seguent : <br> <p>Per confirmar vòstre inscripcion, mercés de clicar sul ligam seguent:<br>
<%= link_to 'Confirmar mon compte', confirmation_url(@resource, confirmation_token: @token) %></p> <%= link_to 'Confirmar mon compte', confirmation_url(@resource, confirmation_token: @token) %></p>
<p>Aprèp vòstra primièra connexion, poiretz accedir a la documentacion de laisina.</p> <p>Aprèp vòstra primièra connexion, poiretz accedir a la documentacion de laisina.</p>

View File

@@ -1,8 +1,8 @@
Bonjorn <%= @resource.email %>&nbsp;! Bonjorn <%= @resource.email %>!
Venètz de vos crear un compte sus <%= @instance %> e vos mercegem :) Venètz de vos crear un compte sus <%= @instance %> e vos mercegem :)
er confirmar vòstre inscripcion, mercés de clicar sul ligam seguent : er confirmar vòstre inscripcion, mercés de clicar sul ligam seguent:
<%= link_to 'Confirmar mon compte', confirmation_url(@resource, confirmation_token: @token) %> <%= link_to 'Confirmar mon compte', confirmation_url(@resource, confirmation_token: @token) %>
Aprèp vòstra primièra connexion, poiretz accedir a la documentacion de laisina. Aprèp vòstra primièra connexion, poiretz accedir a la documentacion de laisina.

View File

@@ -1,3 +1,3 @@
<p>Bonjorn <%= @resource.email %>&nbsp;!</p> <p>Bonjorn <%= @resource.email %>!</p>
<p>Vos contactem per vos avisar que vòstre senhal per Mastodon es ben estat cambiat.</p> <p>Vos contactem per vos avisar quavèm ben cambiat vòstre senhal Mastodon.</p>

View File

@@ -1,3 +1,3 @@
Bonjorn <%= @resource.email %>&nbsp;! Bonjorn <%= @resource.email %>!
Vos contactem per vos avisar que vòstre senhal per Mastodon es ben estat cambiat. Vos contactem per vos avisar quavèm ben cambiat vòstre senhal Mastodon.

View File

@@ -1,4 +1,4 @@
<p>Bonjorn <%= @resource.email %>&nbsp;!</p> <p>Bonjorn <%= @resource.email %>!</p>
<p>Qualquun a demandat una reĩnicializacion de vòstre senhal per Mastodon. Podètz realizar la reĩnicializacion en clicant sul ligam çai-jos.</p> <p>Qualquun a demandat una reĩnicializacion de vòstre senhal per Mastodon. Podètz realizar la reĩnicializacion en clicant sul ligam çai-jos.</p>

View File

@@ -1,4 +1,4 @@
Bonjorn <%= @resource.email %>&nbsp;! Bonjorn <%= @resource.email %>!
Qualquun a demandat una reĩnicializacion de vòstre senhal per Mastodon. Podètz realizar la reĩnicializacion en clicant sul ligam çai-jos.</p> Qualquun a demandat una reĩnicializacion de vòstre senhal per Mastodon. Podètz realizar la reĩnicializacion en clicant sul ligam çai-jos.</p>

View File

@@ -9,6 +9,9 @@ end
Sidekiq.configure_server do |config| Sidekiq.configure_server do |config|
config.redis = redis_params config.redis = redis_params
config.client_middleware do |chain|
chain.add Mastodon::UniqueRetryJobMiddleware
end
end end
Sidekiq.configure_client do |config| Sidekiq.configure_client do |config|

View File

@@ -451,11 +451,11 @@ pl:
show_more: Pokaż więcej show_more: Pokaż więcej
visibilities: visibilities:
private: Tylko dla śledzących private: Tylko dla śledzących
private_long: Widoczny tylko dla osób, które Cię śledzą private_long: Widoczne tylko dla osób, które Cię śledzą
public: Publiczny public: Publiczne
public_long: Widoczny dla wszystkich użytkowników public_long: Widoczne dla wszystkich użytkowników
unlisted: Niewypisany unlisted: Niewypisane
unlisted_long: Widoczny dla wszystkich, ale nie wyświetlany na publicznych osiach czasu unlisted_long: Widoczne dla wszystkich, ale nie wyświetlane na publicznych osiach czasu
stream_entries: stream_entries:
click_to_show: Naciśnij aby wyświetlić click_to_show: Naciśnij aby wyświetlić
pinned: Przypięty wpis pinned: Przypięty wpis

View File

@@ -1,5 +1,5 @@
class AddLocalToStatuses < ActiveRecord::Migration[5.1] class AddLocalToStatuses < ActiveRecord::Migration[5.1]
def change def change
add_column :statuses, :local, :boolean, null: true, default: false add_column :statuses, :local, :boolean, null: true, default: nil
end end
end end

View File

@@ -315,7 +315,7 @@ ActiveRecord::Schema.define(version: 20170905165803) do
t.integer "reblogs_count", default: 0, null: false t.integer "reblogs_count", default: 0, null: false
t.string "language" t.string "language"
t.bigint "conversation_id" t.bigint "conversation_id"
t.boolean "local", default: false t.boolean "local"
t.index ["account_id", "id"], name: "index_statuses_on_account_id_id" t.index ["account_id", "id"], name: "index_statuses_on_account_id_id"
t.index ["conversation_id"], name: "index_statuses_on_conversation_id" t.index ["conversation_id"], name: "index_statuses_on_conversation_id"
t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id" t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id"

View File

@@ -21,7 +21,7 @@ module Mastodon
end end
def flags def flags
'rc2' 'rc4'
end end
def to_a def to_a

View File

@@ -47,7 +47,7 @@ namespace :mastodon do
confirm = STDIN.gets.chomp confirm = STDIN.gets.chomp
puts puts
if confirm.casecmp?('y') if confirm.casecmp('y').zero?
password = SecureRandom.hex password = SecureRandom.hex
user = User.new(email: email, password: password, account_attributes: { username: username }) user = User.new(email: email, password: password, account_attributes: { username: username })
if user.save if user.save
@@ -289,13 +289,13 @@ namespace :mastodon do
puts 'Delete records and associated files from deprecated preview cards? [y/N]: ' puts 'Delete records and associated files from deprecated preview cards? [y/N]: '
confirm = STDIN.gets.chomp confirm = STDIN.gets.chomp
if confirm.casecmp?('y') if confirm.casecmp('y').zero?
DeprecatedPreviewCard.in_batches.destroy_all DeprecatedPreviewCard.in_batches.destroy_all
puts 'Drop deprecated preview cards table? [y/N]: ' puts 'Drop deprecated preview cards table? [y/N]: '
confirm = STDIN.gets.chomp confirm = STDIN.gets.chomp
if confirm.casecmp?('y') if confirm.casecmp('y').zero?
ActiveRecord::Migration.drop_table :deprecated_preview_cards ActiveRecord::Migration.drop_table :deprecated_preview_cards
end end
end end

43
public/embed.js Normal file
View File

@@ -0,0 +1,43 @@
(function() {
'use strict';
var ready = function(loaded) {
if (['interactive', 'complete'].indexOf(document.readyState) !== -1) {
loaded();
} else {
document.addEventListener('DOMContentLoaded', loaded);
}
};
ready(function() {
var iframes = [];
window.addEventListener('message', function(e) {
var data = e.data || {};
if (data.type !== 'setHeight' || !iframes[data.id]) {
return;
}
iframes[data.id].height = data.height;
});
[].forEach.call(document.querySelectorAll('iframe.mastodon-embed'), function(iframe) {
iframe.scrolling = 'no';
iframe.style.overflow = 'hidden';
iframes.push(iframe);
var id = iframes.length - 1;
iframe.onload = function() {
iframe.contentWindow.postMessage({
type: 'setHeight',
id: id,
}, '*');
};
iframe.onload();
});
});
})();

View File

@@ -4,10 +4,10 @@ require 'rails_helper'
describe SettingsHelper do describe SettingsHelper do
describe 'the HUMAN_LOCALES constant' do describe 'the HUMAN_LOCALES constant' do
it 'has the same number of keys as I18n locales exist' do it 'includes all I18n locales' do
options = I18n.available_locales options = I18n.available_locales
expect(described_class::HUMAN_LOCALES.keys).to eq(options) expect(described_class::HUMAN_LOCALES.keys).to include(*options)
end end
end end

View File

@@ -2,12 +2,16 @@ require 'rails_helper'
RSpec.describe ActivityPub::Activity::Update do RSpec.describe ActivityPub::Activity::Update do
let!(:sender) { Fabricate(:account) } let!(:sender) { Fabricate(:account) }
before do before do
stub_request(:get, actor_json[:outbox]).to_return(status: 404)
stub_request(:get, actor_json[:followers]).to_return(status: 404)
stub_request(:get, actor_json[:following]).to_return(status: 404)
sender.update!(uri: ActivityPub::TagManager.instance.uri_for(sender)) sender.update!(uri: ActivityPub::TagManager.instance.uri_for(sender))
end end
let(:modified_sender) do let(:modified_sender) do
sender.dup.tap do |modified_sender| sender.dup.tap do |modified_sender|
modified_sender.display_name = 'Totally modified now' modified_sender.display_name = 'Totally modified now'
end end

View File

@@ -91,9 +91,35 @@ RSpec.describe ActivityPub::TagManager do
end end
describe '#uri_to_resource' do describe '#uri_to_resource' do
it 'returns the local resource' do it 'returns the local account' do
account = Fabricate(:account) account = Fabricate(:account)
expect(subject.uri_to_resource(subject.uri_for(account), Account)).to eq account expect(subject.uri_to_resource(subject.uri_for(account), Account)).to eq account
end end
it 'returns the remote account by matching URI without fragment part' do
account = Fabricate(:account, uri: 'https://example.com/123')
expect(subject.uri_to_resource('https://example.com/123#456', Account)).to eq account
end
it 'returns the local status for ActivityPub URI' do
status = Fabricate(:status)
expect(subject.uri_to_resource(subject.uri_for(status), Status)).to eq status
end
it 'returns the local status for OStatus tag: URI' do
status = Fabricate(:status)
expect(subject.uri_to_resource(::TagManager.instance.uri_for(status), Status)).to eq status
end
it 'returns the local status for OStatus StreamEntry URL' do
status = Fabricate(:status)
stream_entry_url = account_stream_entry_url(status.account, status.stream_entry)
expect(subject.uri_to_resource(stream_entry_url, Status)).to eq status
end
it 'returns the remote status by matching URI without fragment part' do
status = Fabricate(:status, uri: 'https://example.com/123')
expect(subject.uri_to_resource('https://example.com/123#456', Status)).to eq status
end
end end
end end

View File

@@ -21,4 +21,18 @@ describe Report do
expect(report.media_attachments).to eq [media_attachment] expect(report.media_attachments).to eq [media_attachment]
end end
end end
describe 'validatiions' do
it 'has a valid fabricator' do
report = Fabricate(:report)
report.valid?
expect(report).to be_valid
end
it 'is invalid if comment is longer than 1000 characters' do
report = Fabricate.build(:report, comment: Faker::Lorem.characters(1001))
report.valid?
expect(report).to model_have_error_on_field(:comment)
end
end
end end

View File

@@ -41,7 +41,7 @@ RSpec.describe ActivityPub::FetchRemoteAccountService do
before do before do
actor[:inbox] = nil actor[:inbox] = nil
stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor)) stub_request(:get, 'https://example.com/alice').to_return(body: Oj.dump(actor))
stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' }) stub_request(:get, 'https://example.com/.well-known/webfinger?resource=acct:alice@example.com').to_return(body: Oj.dump(webfinger), headers: { 'Content-Type': 'application/jrd+json' })
end end

View File

@@ -26,7 +26,7 @@ RSpec.describe UnsubscribeService do
stub_request(:post, 'http://hub.example.com/').to_raise(HTTP::Error) stub_request(:post, 'http://hub.example.com/').to_raise(HTTP::Error)
subject.call(account) subject.call(account)
expect(logger).to have_received(:debug).with(/PuSH subscription request for bob@example.com could not be made due to HTTP or SSL error/) expect(logger).to have_received(:debug).with(/unsubscribe for bob@example.com failed/)
end end
def stub_logger def stub_logger