Merge pull request #142 from glitch-soc/sync/upstream-1.6.0rc4
Merge with 1.6.0rc4 STRAP IN BUCKAWOO HERE WE GO AGAIN
This commit is contained in:
		
						commit
						c9df53044a
					
				
							
								
								
									
										1
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								Gemfile
									
									
									
									
									
								
							| @ -24,6 +24,7 @@ gem 'addressable', '~> 2.5' | ||||
| gem 'bootsnap' | ||||
| gem 'browser' | ||||
| gem 'charlock_holmes', '~> 0.7.5' | ||||
| gem 'iso-639' | ||||
| gem 'cld3', '~> 3.1' | ||||
| gem 'devise', '~> 4.2' | ||||
| gem 'devise-two-factor', '~> 3.0' | ||||
|  | ||||
| @ -225,6 +225,7 @@ GEM | ||||
|       terminal-table (>= 1.5.1) | ||||
|     idn-ruby (0.1.0) | ||||
|     ipaddress (0.8.3) | ||||
|     iso-639 (0.2.8) | ||||
|     jmespath (1.3.1) | ||||
|     json (2.1.0) | ||||
|     json-ld (2.1.5) | ||||
| @ -560,6 +561,7 @@ DEPENDENCIES | ||||
|   httplog (~> 0.99) | ||||
|   i18n-tasks (~> 0.9) | ||||
|   idn-ruby | ||||
|   iso-639 | ||||
|   json-ld-preloaded (~> 2.2.1) | ||||
|   kaminari (~> 1.0) | ||||
|   letter_opener (~> 1.4) | ||||
|  | ||||
| @ -30,6 +30,7 @@ module SettingsHelper | ||||
|     th: 'ภาษาไทย', | ||||
|     tr: 'Türkçe', | ||||
|     uk: 'Українська', | ||||
|     zh: '中文', | ||||
|     'zh-CN': '简体中文', | ||||
|     'zh-HK': '繁體中文(香港)', | ||||
|     'zh-TW': '繁體中文(臺灣)', | ||||
| @ -39,6 +40,10 @@ module SettingsHelper | ||||
|     HUMAN_LOCALES[locale] | ||||
|   end | ||||
| 
 | ||||
|   def filterable_languages | ||||
|     I18n.available_locales.map { |locale| locale.to_s.split('-').first.to_sym }.uniq | ||||
|   end | ||||
| 
 | ||||
|   def hash_to_object(hash) | ||||
|     HashObject.new(hash) | ||||
|   end | ||||
|  | ||||
| @ -1,6 +1,11 @@ | ||||
| import api from '../api'; | ||||
| 
 | ||||
| import { updateTimeline } from './timelines'; | ||||
| import { | ||||
|   updateTimeline, | ||||
|   refreshHomeTimeline, | ||||
|   refreshCommunityTimeline, | ||||
|   refreshPublicTimeline, | ||||
| } from './timelines'; | ||||
| 
 | ||||
| export const COMPOSE_CHANGE          = 'COMPOSE_CHANGE'; | ||||
| export const COMPOSE_SUBMIT_REQUEST  = 'COMPOSE_SUBMIT_REQUEST'; | ||||
| @ -98,16 +103,20 @@ export function submitCompose() { | ||||
|       dispatch(submitComposeSuccess({ ...response.data })); | ||||
| 
 | ||||
|       // 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 (getState().getIn(['timelines', 'community', 'loaded'])) { | ||||
|           dispatch(updateTimeline('community', { ...response.data })); | ||||
|         } | ||||
| 
 | ||||
|         if (getState().getIn(['timelines', 'public', 'loaded'])) { | ||||
|           dispatch(updateTimeline('public', { ...response.data })); | ||||
|         } | ||||
|         insertOrRefresh('community', refreshCommunityTimeline); | ||||
|         insertOrRefresh('public', refreshPublicTimeline); | ||||
|       } | ||||
|     }).catch(function (error) { | ||||
|       dispatch(submitComposeFail(error)); | ||||
|  | ||||
							
								
								
									
										39
									
								
								app/javascript/mastodon/actions/pin_statuses.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								app/javascript/mastodon/actions/pin_statuses.js
									
									
									
									
									
										Normal 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, | ||||
|   }; | ||||
| }; | ||||
| @ -27,6 +27,10 @@ export default class ScrollableList extends PureComponent { | ||||
|     trackScroll: true, | ||||
|   }; | ||||
| 
 | ||||
|   state = { | ||||
|     lastMouseMove: null, | ||||
|   }; | ||||
| 
 | ||||
|   intersectionObserverWrapper = new IntersectionObserverWrapper(); | ||||
| 
 | ||||
|   handleScroll = throttle(() => { | ||||
| @ -47,6 +51,14 @@ export default class ScrollableList extends PureComponent { | ||||
|     trailing: true, | ||||
|   }); | ||||
| 
 | ||||
|   handleMouseMove = throttle(() => { | ||||
|     this._lastMouseMove = new Date(); | ||||
|   }, 300); | ||||
| 
 | ||||
|   handleMouseLeave = () => { | ||||
|     this._lastMouseMove = null; | ||||
|   } | ||||
| 
 | ||||
|   componentDidMount () { | ||||
|     this.attachScrollListener(); | ||||
|     this.attachIntersectionObserver(); | ||||
| @ -56,11 +68,15 @@ export default class ScrollableList extends PureComponent { | ||||
|   } | ||||
| 
 | ||||
|   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
 | ||||
|     // 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 (this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props)) { | ||||
|     if (someItemInserted && this._oldScrollPosition && this.node.scrollTop > 0) { | ||||
|       const newScrollTop = this.node.scrollHeight - this._oldScrollPosition; | ||||
| 
 | ||||
|       if (this.node.scrollTop !== newScrollTop) { | ||||
|         this.node.scrollTop = newScrollTop; | ||||
|       } | ||||
| @ -68,7 +84,6 @@ export default class ScrollableList extends PureComponent { | ||||
|       this._oldScrollPosition = this.node.scrollHeight - this.node.scrollTop; | ||||
|     } | ||||
|   } | ||||
|   } | ||||
| 
 | ||||
|   componentWillUnmount () { | ||||
|     this.detachScrollListener(); | ||||
| @ -114,6 +129,10 @@ export default class ScrollableList extends PureComponent { | ||||
|     this.props.onScrollToBottom(); | ||||
|   } | ||||
| 
 | ||||
|   _recentlyMoved () { | ||||
|     return this._lastMouseMove !== null && ((new Date()) - this._lastMouseMove < 600); | ||||
|   } | ||||
| 
 | ||||
|   handleKeyDown = (e) => { | ||||
|     if (['PageDown', 'PageUp'].includes(e.key) || (e.ctrlKey && ['End', 'Home'].includes(e.key))) { | ||||
|       const article = (() => { | ||||
| @ -149,7 +168,7 @@ export default class ScrollableList extends PureComponent { | ||||
| 
 | ||||
|     if (isLoading || childrenCount > 0 || !emptyMessage) { | ||||
|       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}> | ||||
|             {prepend} | ||||
| 
 | ||||
|  | ||||
| @ -137,7 +137,7 @@ export default class StatusActionBar extends ImmutablePureComponent { | ||||
| 
 | ||||
|     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(null); | ||||
|     } | ||||
|  | ||||
| @ -149,29 +149,29 @@ export default class VideoPlayer extends React.PureComponent { | ||||
|     if (!this.state.visible) { | ||||
|       if (sensitive) { | ||||
|         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} | ||||
|             <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> | ||||
|           </div> | ||||
|           </button> | ||||
|         ); | ||||
|       } else { | ||||
|         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} | ||||
|             <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> | ||||
|           </div> | ||||
|           </button> | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     if (this.state.preview && !autoplay) { | ||||
|       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} | ||||
|           <div className='media-spoiler-video-play-icon'><i className='fa fa-play' /></div> | ||||
|         </div> | ||||
|         </button> | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -26,6 +26,7 @@ const messages = defineMessages({ | ||||
|   mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, | ||||
|   info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' }, | ||||
|   show_me_around: { id: 'getting_started.onboarding', defaultMessage: 'Show me around' }, | ||||
|   pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' }, | ||||
| }); | ||||
| 
 | ||||
| const mapStateToProps = state => ({ | ||||
| @ -79,15 +80,16 @@ export default class GettingStarted extends ImmutablePureComponent { | ||||
| 
 | ||||
|     navItems = navItems.concat([ | ||||
|       <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')) { | ||||
|       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([ | ||||
|       <ColumnLink key='6' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />, | ||||
|       <ColumnLink key='7' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />, | ||||
|       <ColumnLink key='7' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />, | ||||
|       <ColumnLink key='8' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />, | ||||
|     ]); | ||||
| 
 | ||||
|     return ( | ||||
|  | ||||
							
								
								
									
										59
									
								
								app/javascript/mastodon/features/pinned_statuses/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								app/javascript/mastodon/features/pinned_statuses/index.js
									
									
									
									
									
										Normal 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> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| @ -33,7 +33,8 @@ export default class EmbedModal extends ImmutablePureComponent { | ||||
|       iframeDocument.close(); | ||||
| 
 | ||||
|       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 | ||||
|             className='embed-modal__iframe' | ||||
|             scrolling='no' | ||||
|             frameBorder='0' | ||||
|             ref={this.setIframeRef} | ||||
|             title='preview' | ||||
|  | ||||
| @ -36,6 +36,7 @@ import { | ||||
|   FavouritedStatuses, | ||||
|   Blocks, | ||||
|   Mutes, | ||||
|   PinnedStatuses, | ||||
| } from './util/async-components'; | ||||
| 
 | ||||
| // Dummy import, to make sure that <Status /> ends up in the application bundle.
 | ||||
| @ -235,6 +236,7 @@ export default class UI extends React.PureComponent { | ||||
| 
 | ||||
|             <WrappedRoute path='/notifications' component={Notifications} content={children} /> | ||||
|             <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} /> | ||||
|             <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} /> | ||||
| 
 | ||||
|             <WrappedRoute path='/statuses/new' component={Compose} content={children} /> | ||||
|             <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} /> | ||||
|  | ||||
| @ -34,6 +34,10 @@ export function GettingStarted () { | ||||
|   return import(/* webpackChunkName: "features/getting_started" */'../../getting_started'); | ||||
| } | ||||
| 
 | ||||
| export function PinnedStatuses () { | ||||
|   return import(/* webpackChunkName: "features/pinned_statuses" */'../../pinned_statuses'); | ||||
| } | ||||
| 
 | ||||
| export function AccountTimeline () { | ||||
|   return import(/* webpackChunkName: "features/account_timeline" */'../../account_timeline'); | ||||
| } | ||||
|  | ||||
| @ -34,6 +34,7 @@ | ||||
|   "column.mutes": "Muted users", | ||||
|   "column.notifications": "Notifications", | ||||
|   "column.public": "Federated timeline", | ||||
|   "column.pins": "Pinned toots", | ||||
|   "column_back_button.label": "Back", | ||||
|   "column_header.hide_settings": "Hide settings", | ||||
|   "column_header.moveLeft_settings": "Move column to the left", | ||||
| @ -111,6 +112,7 @@ | ||||
|   "navigation_bar.mutes": "Muted users", | ||||
|   "navigation_bar.preferences": "Preferences", | ||||
|   "navigation_bar.public_timeline": "Federated timeline", | ||||
|   "navigation_bar.pins": "Pinned toots", | ||||
|   "notification.favourite": "{name} favourited your status", | ||||
|   "notification.follow": "{name} followed you", | ||||
|   "notification.mention": "{name} mentioned you", | ||||
|  | ||||
| @ -34,6 +34,7 @@ | ||||
|   "column.mutes": "Comptes masqués", | ||||
|   "column.notifications": "Notifications", | ||||
|   "column.public": "Fil public global", | ||||
|   "column.pins": "Pouets épinglés", | ||||
|   "column_back_button.label": "Retour", | ||||
|   "column_header.hide_settings": "Masquer les paramètres", | ||||
|   "column_header.moveLeft_settings": "Déplacer la colonne vers la gauche", | ||||
| @ -62,9 +63,9 @@ | ||||
|   "confirmations.mute.confirm": "Masquer", | ||||
|   "confirmations.mute.message": "Confirmez vous le masquage de {name} ?", | ||||
|   "confirmations.unfollow.confirm": "Ne plus suivre", | ||||
|   "confirmations.unfollow.message": "Vous voulez-vous arrêter de suivre {name} ?", | ||||
|   "embed.instructions": "Embed this status on your website by copying the code below.", | ||||
|   "embed.preview": "Here is what it will look like:", | ||||
|   "confirmations.unfollow.message": "Voulez-vous arrêter de suivre {name} ?", | ||||
|   "embed.instructions": "Intégrez ce statut à votre site en copiant ce code ci-dessous.", | ||||
|   "embed.preview": "Il apparaîtra comme cela : ", | ||||
|   "emoji_button.activity": "Activités", | ||||
|   "emoji_button.flags": "Drapeaux", | ||||
|   "emoji_button.food": "Boire et manger", | ||||
| @ -111,6 +112,7 @@ | ||||
|   "navigation_bar.mutes": "Comptes masqués", | ||||
|   "navigation_bar.preferences": "Préférences", | ||||
|   "navigation_bar.public_timeline": "Fil public global", | ||||
|   "navigation_bar.pins": "Pouets épinglés", | ||||
|   "notification.favourite": "{name} a ajouté à ses favoris :", | ||||
|   "notification.follow": "{name} vous suit.", | ||||
|   "notification.mention": "{name} vous a mentionné⋅e :", | ||||
|  | ||||
| @ -34,6 +34,7 @@ | ||||
|   "column.mutes": "ミュートしたユーザー", | ||||
|   "column.notifications": "通知", | ||||
|   "column.public": "連合タイムライン", | ||||
|   "column.pins": "固定されたトゥート", | ||||
|   "column_back_button.label": "戻る", | ||||
|   "column_header.hide_settings": "設定を隠す", | ||||
|   "column_header.moveLeft_settings": "カラムを左に移動する", | ||||
| @ -111,6 +112,7 @@ | ||||
|   "navigation_bar.mutes": "ミュートしたユーザー", | ||||
|   "navigation_bar.preferences": "ユーザー設定", | ||||
|   "navigation_bar.public_timeline": "連合タイムライン", | ||||
|   "navigation_bar.pins": "固定されたトゥート", | ||||
|   "notification.favourite": "{name}さんがあなたのトゥートをお気に入りに登録しました", | ||||
|   "notification.follow": "{name}さんにフォローされました", | ||||
|   "notification.mention": "{name}さんがあなたに返信しました", | ||||
|  | ||||
| @ -34,6 +34,7 @@ | ||||
|   "column.mutes": "뮤트 중인 사용자", | ||||
|   "column.notifications": "알림", | ||||
|   "column.public": "연합 타임라인", | ||||
|   "column.pins": "고정된 Toot", | ||||
|   "column_back_button.label": "돌아가기", | ||||
|   "column_header.hide_settings": "Hide settings", | ||||
|   "column_header.moveLeft_settings": "Move column to the left", | ||||
| @ -111,6 +112,7 @@ | ||||
|   "navigation_bar.mutes": "뮤트 중인 사용자", | ||||
|   "navigation_bar.preferences": "사용자 설정", | ||||
|   "navigation_bar.public_timeline": "연합 타임라인", | ||||
|   "navigation_bar.pins": "고정된 Toot", | ||||
|   "notification.favourite": "{name}님이 즐겨찾기 했습니다", | ||||
|   "notification.follow": "{name}님이 나를 팔로우 했습니다", | ||||
|   "notification.mention": "{name}님이 답글을 보냈습니다", | ||||
|  | ||||
| @ -34,6 +34,7 @@ | ||||
|   "column.mutes": "Personas en silenci", | ||||
|   "column.notifications": "Notificacions", | ||||
|   "column.public": "Flux public global", | ||||
|   "column.pins": "Tuts penjats", | ||||
|   "column_back_button.label": "Tornar", | ||||
|   "column_header.hide_settings": "Amagar los paramètres", | ||||
|   "column_header.moveLeft_settings": "Desplaçar la colomna a man drecha", | ||||
| @ -111,6 +112,7 @@ | ||||
|   "navigation_bar.mutes": "Personas rescondudas", | ||||
|   "navigation_bar.preferences": "Preferéncias", | ||||
|   "navigation_bar.public_timeline": "Flux public global", | ||||
|   "navigation_bar.pins": "Tuts penjats", | ||||
|   "notification.favourite": "{name} a ajustat a sos favorits :", | ||||
|   "notification.follow": "{name} vos sèc", | ||||
|   "notification.mention": "{name} vos a mencionat :", | ||||
| @ -126,21 +128,21 @@ | ||||
|   "notifications.column_settings.reblog": "Partatges :", | ||||
|   "notifications.column_settings.show": "Mostrar dins la colomna", | ||||
|   "notifications.column_settings.sound": "Emetre un son", | ||||
|   "onboarding.done": "Fach", | ||||
|   "onboarding.done": "Sortir", | ||||
|   "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 d’acuèlh mòstra los estatuts del mond que seguètz.", | ||||
|   "onboarding.page_four.notifications": "La colomna de notificacions vos fa veire quand qualqu’un 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.welcome": "Benvengut a Mastodon !", | ||||
|   "onboarding.page_six.admin": "Vòstre administrator d’instància es {admin}.", | ||||
|   "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 d’aplicacions 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.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_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 d’una autra instància, picatz son identificant complet.", | ||||
| @ -183,7 +185,7 @@ | ||||
|   "status.show_less": "Tornar plegar", | ||||
|   "status.show_more": "Desplegar", | ||||
|   "status.unmute_conversation": "Conversacions amb silenci levat", | ||||
|   "status.unpin": "Despenjar del perfil", | ||||
|   "status.unpin": "Tirar del perfil", | ||||
|   "tabs_bar.compose": "Compausar", | ||||
|   "tabs_bar.federated_timeline": "Flux public global", | ||||
|   "tabs_bar.home": "Acuèlh", | ||||
|  | ||||
| @ -12,7 +12,7 @@ | ||||
|   "account.mute": "Wycisz @{name}", | ||||
|   "account.posts": "Wpisy", | ||||
|   "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.unblock": "Odblokuj @{name}", | ||||
|   "account.unblock_domain": "Odblokuj domenę {domain}", | ||||
| @ -33,6 +33,7 @@ | ||||
|   "column.home": "Strona główna", | ||||
|   "column.mutes": "Wyciszeni użytkownicy", | ||||
|   "column.notifications": "Powiadomienia", | ||||
|   "column.pins": "Przypięte wpisy", | ||||
|   "column.public": "Globalna oś czasu", | ||||
|   "column_back_button.label": "Wróć", | ||||
|   "column_header.hide_settings": "Ukryj ustawienia", | ||||
| @ -109,6 +110,7 @@ | ||||
|   "navigation_bar.info": "Szczegółowe informacje", | ||||
|   "navigation_bar.logout": "Wyloguj", | ||||
|   "navigation_bar.mutes": "Wyciszeni użytkownicy", | ||||
|   "navigation_bar.pins": "Przypięte wpisy", | ||||
|   "navigation_bar.preferences": "Preferencje", | ||||
|   "navigation_bar.public_timeline": "Oś czasu federacji", | ||||
|   "notification.favourite": "{name} dodał Twój status do ulubionych", | ||||
| @ -153,8 +155,8 @@ | ||||
|   "privacy.private.short": "Tylko dla śledzących", | ||||
|   "privacy.public.long": "Widoczny na publicznych osiach czasu", | ||||
|   "privacy.public.short": "Publiczny", | ||||
|   "privacy.unlisted.long": "Niewidoczne na publicznych osiach czasu", | ||||
|   "privacy.unlisted.short": "Niewidoczne", | ||||
|   "privacy.unlisted.long": "Niewidoczny na publicznych osiach czasu", | ||||
|   "privacy.unlisted.short": "Niewidoczny", | ||||
|   "reply_indicator.cancel": "Anuluj", | ||||
|   "report.placeholder": "Dodatkowe komentarze", | ||||
|   "report.submit": "Wyślij", | ||||
|  | ||||
| @ -28,7 +28,7 @@ export default function reports(state = initialState, action) { | ||||
|       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', 'comment'], ''); | ||||
|       } else { | ||||
|       } else if (action.status) { | ||||
|         map.updateIn(['new', 'status_ids'], ImmutableSet(), set => set.add(action.status.getIn(['reblog', 'id'], action.status.get('id')))); | ||||
|       } | ||||
|     }); | ||||
|  | ||||
| @ -2,10 +2,15 @@ import { | ||||
|   FAVOURITED_STATUSES_FETCH_SUCCESS, | ||||
|   FAVOURITED_STATUSES_EXPAND_SUCCESS, | ||||
| } from '../actions/favourites'; | ||||
| import { | ||||
|   PINNED_STATUSES_FETCH_SUCCESS, | ||||
| } from '../actions/pin_statuses'; | ||||
| import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; | ||||
| import { | ||||
|   FAVOURITE_SUCCESS, | ||||
|   UNFAVOURITE_SUCCESS, | ||||
|   PIN_SUCCESS, | ||||
|   UNPIN_SUCCESS, | ||||
| } from '../actions/interactions'; | ||||
| 
 | ||||
| const initialState = ImmutableMap({ | ||||
| @ -14,6 +19,11 @@ const initialState = ImmutableMap({ | ||||
|     loaded: false, | ||||
|     items: ImmutableList(), | ||||
|   }), | ||||
|   pins: ImmutableMap({ | ||||
|     next: null, | ||||
|     loaded: false, | ||||
|     items: ImmutableList(), | ||||
|   }), | ||||
| }); | ||||
| 
 | ||||
| const normalizeList = (state, listType, statuses, next) => { | ||||
| @ -53,6 +63,12 @@ export default function statusLists(state = initialState, action) { | ||||
|     return prependOneToList(state, 'favourites', action.status); | ||||
|   case UNFAVOURITE_SUCCESS: | ||||
|     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: | ||||
|     return state; | ||||
|   } | ||||
|  | ||||
| @ -36,6 +36,9 @@ import { | ||||
|   FAVOURITED_STATUSES_FETCH_SUCCESS, | ||||
|   FAVOURITED_STATUSES_EXPAND_SUCCESS, | ||||
| } from '../actions/favourites'; | ||||
| import { | ||||
|   PINNED_STATUSES_FETCH_SUCCESS, | ||||
| } from '../actions/pin_statuses'; | ||||
| import { SEARCH_FETCH_SUCCESS } from '../actions/search'; | ||||
| import emojify from '../emoji'; | ||||
| import { Map as ImmutableMap, fromJS } from 'immutable'; | ||||
| @ -138,6 +141,7 @@ export default function statuses(state = initialState, action) { | ||||
|   case NOTIFICATIONS_EXPAND_SUCCESS: | ||||
|   case FAVOURITED_STATUSES_FETCH_SUCCESS: | ||||
|   case FAVOURITED_STATUSES_EXPAND_SUCCESS: | ||||
|   case PINNED_STATUSES_FETCH_SUCCESS: | ||||
|   case SEARCH_FETCH_SUCCESS: | ||||
|     return normalizeStatuses(state, action.statuses); | ||||
|   case TIMELINE_DELETE: | ||||
|  | ||||
| @ -1,5 +1,22 @@ | ||||
| import loadPolyfills from '../mastodon/load_polyfills'; | ||||
| import { processBio } from '../glitch/util/bio_metadata'; | ||||
| 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() { | ||||
|   const { length } = require('stringz'); | ||||
| @ -7,13 +24,13 @@ function main() { | ||||
|   const { delegate } = require('rails-ujs'); | ||||
|   const emojify = require('../mastodon/emoji').default; | ||||
|   const { getLocale } = require('../mastodon/locales'); | ||||
|   const ready = require('../mastodon/ready').default; | ||||
| 
 | ||||
|   const { localeData } = getLocale(); | ||||
| 
 | ||||
|   localeData.forEach(IntlRelativeFormat.__addLocaleData); | ||||
| 
 | ||||
|   ready(() => { | ||||
|     const locale = document.documentElement.lang; | ||||
| 
 | ||||
|     const dateTimeFormat = new Intl.DateTimeFormat(locale, { | ||||
|       year: 'numeric', | ||||
|       month: 'long', | ||||
| @ -21,6 +38,7 @@ function main() { | ||||
|       hour: 'numeric', | ||||
|       minute: 'numeric', | ||||
|     }); | ||||
| 
 | ||||
|     const relativeFormat = new IntlRelativeFormat(locale); | ||||
| 
 | ||||
|     [].forEach.call(document.querySelectorAll('.emojify'), (content) => { | ||||
| @ -30,12 +48,14 @@ function main() { | ||||
|     [].forEach.call(document.querySelectorAll('time.formatted'), (content) => { | ||||
|       const datetime = new Date(content.getAttribute('datetime')); | ||||
|       const formattedDate = dateTimeFormat.format(datetime); | ||||
| 
 | ||||
|       content.title = formattedDate; | ||||
|       content.textContent = formattedDate; | ||||
|     }); | ||||
| 
 | ||||
|     [].forEach.call(document.querySelectorAll('time.time-ago'), (content) => { | ||||
|       const datetime = new Date(content.getAttribute('datetime')); | ||||
| 
 | ||||
|       content.title = dateTimeFormat.format(datetime); | ||||
|       content.textContent = relativeFormat.format(datetime); | ||||
|     }); | ||||
| @ -46,10 +66,6 @@ function main() { | ||||
|         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 }) => { | ||||
| @ -78,6 +94,7 @@ function main() { | ||||
| 
 | ||||
|   delegate(document, '.status__content__spoiler-link', 'click', ({ target }) => { | ||||
|     const contentEl = target.parentNode.parentNode.querySelector('.e-content'); | ||||
| 
 | ||||
|     if (contentEl.style.display === 'block') { | ||||
|       contentEl.style.display = 'none'; | ||||
|       target.parentNode.style.marginBottom = 0; | ||||
| @ -85,11 +102,13 @@ function main() { | ||||
|       contentEl.style.display = 'block'; | ||||
|       target.parentNode.style.marginBottom = null; | ||||
|     } | ||||
| 
 | ||||
|     return false; | ||||
|   }); | ||||
| 
 | ||||
|   delegate(document, '.account_display_name', 'input', ({ target }) => { | ||||
|     const nameCounter = document.querySelector('.name-counter'); | ||||
| 
 | ||||
|     if (nameCounter) { | ||||
|       nameCounter.textContent = 30 - length(target.value); | ||||
|     } | ||||
| @ -97,6 +116,7 @@ function main() { | ||||
| 
 | ||||
|   delegate(document, '.account_note', 'input', ({ target }) => { | ||||
|     const noteCounter = document.querySelector('.note-counter'); | ||||
| 
 | ||||
|     if (noteCounter) { | ||||
|       const noteWithoutMetadata = processBio(target.value).text; | ||||
|       noteCounter.textContent = 500 - length(noteWithoutMetadata); | ||||
| @ -107,6 +127,7 @@ function main() { | ||||
|     const avatar = document.querySelector('.card.compact .avatar img'); | ||||
|     const [file] = target.files || []; | ||||
|     const url = URL.createObjectURL(file); | ||||
| 
 | ||||
|     avatar.src = url; | ||||
|   }); | ||||
| 
 | ||||
| @ -114,6 +135,7 @@ function main() { | ||||
|     const header = document.querySelector('.card.compact'); | ||||
|     const [file] = target.files || []; | ||||
|     const url = URL.createObjectURL(file); | ||||
| 
 | ||||
|     header.style.backgroundImage = `url(${url})`; | ||||
|   }); | ||||
| } | ||||
|  | ||||
| @ -190,11 +190,15 @@ | ||||
| 
 | ||||
| .filters { | ||||
|   display: flex; | ||||
|   margin-bottom: 20px; | ||||
|   flex-wrap: wrap; | ||||
| 
 | ||||
|   .filter-subset { | ||||
|     flex: 0 0 auto; | ||||
|     margin-right: 40px; | ||||
|     margin: 0 40px 10px 0; | ||||
| 
 | ||||
|     &:last-child { | ||||
|       margin-bottom: 20px; | ||||
|     } | ||||
| 
 | ||||
|     ul { | ||||
|       margin-top: 5px; | ||||
|  | ||||
| @ -45,6 +45,7 @@ body { | ||||
|   &.embed { | ||||
|     background: transparent; | ||||
|     margin: 0; | ||||
|     padding-bottom: 0; | ||||
| 
 | ||||
|     .container { | ||||
|       position: absolute; | ||||
|  | ||||
| @ -2716,22 +2716,6 @@ button.icon-button.active i.fa-retweet { | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .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 { | ||||
|   display: block; | ||||
|   font-size: 14px; | ||||
| @ -4302,6 +4286,9 @@ button.icon-button.active i.fa-retweet { | ||||
|     margin-left: -68px; | ||||
|     width: calc(100% + 80px); | ||||
|   } | ||||
| 
 | ||||
|   border: 0; | ||||
|   display: block; | ||||
| } | ||||
| 
 | ||||
| .media-spoiler-video-play-icon { | ||||
|  | ||||
| @ -3,7 +3,6 @@ | ||||
|   max-width: 100%; | ||||
|   border-spacing: 0; | ||||
|   border-collapse: collapse; | ||||
|   margin-bottom: 20px; | ||||
| 
 | ||||
|   th, | ||||
|   td { | ||||
| @ -43,17 +42,15 @@ | ||||
|     font-weight: 500; | ||||
|   } | ||||
| 
 | ||||
|   &.inline-table { | ||||
|     td, | ||||
|     th { | ||||
|       padding: 8px 2px; | ||||
|     } | ||||
| 
 | ||||
|     & > tbody > tr:nth-child(odd) > td, | ||||
|     & > tbody > tr:nth-child(odd) > th { | ||||
|   &.inline-table > tbody > tr:nth-child(odd) > td, | ||||
|   &.inline-table > tbody > tr:nth-child(odd) > th { | ||||
|     background: transparent; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .table-wrapper { | ||||
|   overflow: auto; | ||||
|   margin-bottom: 20px; | ||||
| } | ||||
| 
 | ||||
| samp { | ||||
|  | ||||
| @ -96,12 +96,14 @@ class ActivityPub::TagManager | ||||
|       when 'Account' | ||||
|         klass.find_local(uri_to_local_id(uri, :username)) | ||||
|       else | ||||
|         klass.find_by(id: uri_to_local_id(uri)) | ||||
|         StatusFinder.new(uri).status | ||||
|       end | ||||
|     elsif ::TagManager.instance.local_id?(uri) | ||||
|       klass.find_by(id: ::TagManager.instance.unique_tag_to_local_id(uri, klass.to_s)) | ||||
|     else | ||||
|       klass.find_by(uri: uri.split('#').first) | ||||
|     end | ||||
|   rescue ActiveRecord::RecordNotFound | ||||
|     nil | ||||
|   end | ||||
| end | ||||
|  | ||||
| @ -20,7 +20,16 @@ class LanguageDetector | ||||
|   private | ||||
| 
 | ||||
|   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 | ||||
| 
 | ||||
|   def result | ||||
|  | ||||
| @ -22,6 +22,8 @@ class Report < ApplicationRecord | ||||
|   scope :unresolved, -> { where(action_taken: false) } | ||||
|   scope :resolved,   -> { where(action_taken: true) } | ||||
| 
 | ||||
|   validates :comment, length: { maximum: 1000 } | ||||
| 
 | ||||
|   def statuses | ||||
|     Status.where(id: status_ids).includes(:account, :media_attachments, :mentions) | ||||
|   end | ||||
|  | ||||
| @ -63,8 +63,8 @@ class Status < ApplicationRecord | ||||
|   default_scope { recent } | ||||
| 
 | ||||
|   scope :recent, -> { reorder(id: :desc) } | ||||
|   scope :remote, -> { where.not(uri: nil) } | ||||
|   scope :local, -> { where(uri: nil) } | ||||
|   scope :remote, -> { where(local: false).or(where.not(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_reblogs, -> { where('statuses.reblog_of_id IS NULL') } | ||||
|  | ||||
| @ -40,12 +40,12 @@ class OEmbedSerializer < ActiveModel::Serializer | ||||
|     attributes = { | ||||
|       src: embed_short_account_status_url(object.account, object), | ||||
|       class: 'mastodon-embed', | ||||
|       style: 'max-width: 100%; border: none;', | ||||
|       style: 'max-width: 100%; border: 0', | ||||
|       width: width, | ||||
|       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 | ||||
| 
 | ||||
|   def width | ||||
|  | ||||
| @ -13,6 +13,7 @@ class ActivityPub::ProcessAccountService < BaseService | ||||
|     @username    = username | ||||
|     @domain      = domain | ||||
|     @account     = Account.find_by(uri: @uri) | ||||
|     @collections = {} | ||||
| 
 | ||||
|     create_account  if @account.nil? | ||||
|     upgrade_account if @account.ostatus? | ||||
| @ -47,11 +48,14 @@ class ActivityPub::ProcessAccountService < BaseService | ||||
|     @account.url                 = url || @uri | ||||
|     @account.display_name        = @json['name'] || '' | ||||
|     @account.note                = @json['summary'] || '' | ||||
|     @account.avatar_remote_url   = image_url('icon') | ||||
|     @account.header_remote_url   = image_url('image') | ||||
|     @account.avatar_remote_url   = image_url('icon')  unless skip_download? | ||||
|     @account.header_remote_url   = image_url('image') unless skip_download? | ||||
|     @account.public_key          = public_key || '' | ||||
|     @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 | ||||
| 
 | ||||
|   def upgrade_account | ||||
| @ -88,6 +92,33 @@ class ActivityPub::ProcessAccountService < BaseService | ||||
|     value['href'] | ||||
|   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? | ||||
|     domain_block && domain_block.suspend? | ||||
|   end | ||||
|  | ||||
| @ -12,7 +12,7 @@ class FollowService < BaseService | ||||
|     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) | ||||
| 
 | ||||
|     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? | ||||
|       request_follow(source_account, target_account) | ||||
|  | ||||
| @ -27,9 +27,10 @@ class PostStatusService < BaseService | ||||
|                                         thread: in_reply_to, | ||||
|                                         sensitive: options[:sensitive], | ||||
|                                         spoiler_text: options[:spoiler_text] || '', | ||||
|                                         visibility: options[:visibility], | ||||
|                                         visibility: options[:visibility] || account.user&.setting_default_privacy, | ||||
|                                         language: detect_language_for(text, account), | ||||
|                                         application: options[:application]) | ||||
| 
 | ||||
|       attach_media(status, media) | ||||
|     end | ||||
| 
 | ||||
|  | ||||
| @ -5,15 +5,18 @@ class UnsubscribeService < BaseService | ||||
|     return if account.hub_url.blank? | ||||
| 
 | ||||
|     @account = account | ||||
| 
 | ||||
|     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.subscription_expires_at = nil | ||||
|     @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 | ||||
| 
 | ||||
|   private | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| .table-wrapper | ||||
|   %table.table | ||||
|     %tbody | ||||
|       %tr | ||||
|  | ||||
| @ -50,6 +50,7 @@ | ||||
|       %button= t('admin.accounts.search') | ||||
|       = link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative' | ||||
| 
 | ||||
| .table-wrapper | ||||
|   %table.table | ||||
|     %thead | ||||
|       %tr | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| - content_for :page_title do | ||||
|   = @account.acct | ||||
| 
 | ||||
| .table-wrapper | ||||
|   %table.table | ||||
|     %tbody | ||||
|       %tr | ||||
| @ -81,7 +82,6 @@ | ||||
|         %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' } | ||||
|   - if @account.local? | ||||
|     = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| - content_for :page_title do | ||||
|   = t('admin.domain_blocks.title') | ||||
| 
 | ||||
| .table-wrapper | ||||
|   %table.table | ||||
|     %thead | ||||
|       %tr | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| - content_for :page_title do | ||||
|   = t('admin.instances.title') | ||||
| 
 | ||||
| .table-wrapper | ||||
|   %table.table | ||||
|     %thead | ||||
|       %tr | ||||
|  | ||||
| @ -10,6 +10,7 @@ | ||||
| 
 | ||||
| = form_tag do | ||||
| 
 | ||||
|   .table-wrapper | ||||
|     %table.table | ||||
|       %thead | ||||
|         %tr | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| - content_for :page_title do | ||||
|   = t('admin.subscriptions.title') | ||||
| 
 | ||||
| .table-wrapper | ||||
|   %table.table | ||||
|     %thead | ||||
|       %tr | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| %h6= t 'sessions.title' | ||||
| %p.muted-hint= t 'sessions.explanation' | ||||
| 
 | ||||
| .table-wrapper | ||||
|   %table.table.inline-table | ||||
|     %thead | ||||
|       %tr | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| - content_for :page_title do | ||||
|   = t('doorkeeper.authorized_applications.index.title') | ||||
| 
 | ||||
| .table-wrapper | ||||
|   %table.table | ||||
|     %thead | ||||
|       %tr | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| - content_for :page_title do | ||||
|   = t('doorkeeper.applications.index.title') | ||||
| 
 | ||||
| .table-wrapper | ||||
|   %table.table | ||||
|     %thead | ||||
|       %tr | ||||
|  | ||||
| @ -3,6 +3,7 @@ | ||||
| 
 | ||||
| %p.hint= t('applications.warning') | ||||
| 
 | ||||
| .table-wrapper | ||||
|   %table.table | ||||
|     %tbody | ||||
|       %tr   | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| - content_for :page_title do | ||||
|   = t('settings.export') | ||||
| 
 | ||||
| .table-wrapper | ||||
|   %table.table | ||||
|     %tbody | ||||
|       %tr | ||||
|  | ||||
| @ -12,6 +12,7 @@ | ||||
|   %p= t('followers.explanation_html') | ||||
|   %p= t('followers.true_privacy_html') | ||||
| 
 | ||||
|   .table-wrapper | ||||
|     %table.table | ||||
|       %thead | ||||
|         %tr | ||||
|  | ||||
| @ -13,7 +13,7 @@ | ||||
|       selected: I18n.locale | ||||
| 
 | ||||
|     = f.input :filtered_languages, | ||||
|       collection: I18n.available_locales, | ||||
|       collection: filterable_languages, | ||||
|       wrapper: :with_block_label, | ||||
|       include_blank: false, | ||||
|       label_method: lambda { |locale| human_locale(locale) }, | ||||
|  | ||||
| @ -1,8 +1,8 @@ | ||||
| <p>Bonjorn <%= @resource.email %> !<p> | ||||
| <p>Bonjorn <%= @resource.email %> ! <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> | ||||
| 
 | ||||
| <p>Aprèp vòstra primièra connexion, poiretz accedir a la documentacion de l’aisina.</p> | ||||
|  | ||||
| @ -1,8 +1,8 @@ | ||||
| Bonjorn <%= @resource.email %> ! | ||||
| Bonjorn <%= @resource.email %> !  | ||||
| 
 | ||||
| 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) %> | ||||
| 
 | ||||
| Aprèp vòstra primièra connexion, poiretz accedir a la documentacion de l’aisina. | ||||
|  | ||||
| @ -1,3 +1,3 @@ | ||||
| <p>Bonjorn <%= @resource.email %> !</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 qu’avèm ben cambiat vòstre senhal Mastodon.</p> | ||||
|  | ||||
| @ -1,3 +1,3 @@ | ||||
| Bonjorn <%= @resource.email %> ! | ||||
| Bonjorn <%= @resource.email %> !  | ||||
| 
 | ||||
| Vos contactem per vos avisar que vòstre senhal per Mastodon es ben estat cambiat. | ||||
| Vos contactem per vos avisar qu’avèm ben cambiat vòstre senhal Mastodon. | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <p>Bonjorn <%= @resource.email %> !</p> | ||||
| <p>Bonjorn <%= @resource.email %> ! </p> | ||||
| 
 | ||||
| <p>Qualqu’un a demandat una reĩnicializacion de vòstre senhal per Mastodon. Podètz realizar la reĩnicializacion en clicant sul ligam çai-jos.</p> | ||||
| 
 | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| Bonjorn <%= @resource.email %> ! | ||||
| Bonjorn <%= @resource.email %> !  | ||||
| 
 | ||||
| Qualqu’un a demandat una reĩnicializacion de vòstre senhal per Mastodon. Podètz realizar la reĩnicializacion en clicant sul ligam çai-jos.</p> | ||||
| 
 | ||||
|  | ||||
| @ -9,6 +9,9 @@ end | ||||
| 
 | ||||
| Sidekiq.configure_server do |config| | ||||
|   config.redis = redis_params | ||||
|   config.client_middleware do |chain| | ||||
|     chain.add Mastodon::UniqueRetryJobMiddleware | ||||
|   end | ||||
| end | ||||
| 
 | ||||
| Sidekiq.configure_client do |config| | ||||
|  | ||||
| @ -451,11 +451,11 @@ pl: | ||||
|     show_more: Pokaż więcej | ||||
|     visibilities: | ||||
|       private: Tylko dla śledzących | ||||
|       private_long: Widoczny tylko dla osób, które Cię śledzą | ||||
|       public: Publiczny | ||||
|       public_long: Widoczny dla wszystkich użytkowników | ||||
|       unlisted: Niewypisany | ||||
|       unlisted_long: Widoczny dla wszystkich, ale nie wyświetlany na publicznych osiach czasu | ||||
|       private_long: Widoczne tylko dla osób, które Cię śledzą | ||||
|       public: Publiczne | ||||
|       public_long: Widoczne dla wszystkich użytkowników | ||||
|       unlisted: Niewypisane | ||||
|       unlisted_long: Widoczne dla wszystkich, ale nie wyświetlane na publicznych osiach czasu | ||||
|   stream_entries: | ||||
|     click_to_show: Naciśnij aby wyświetlić | ||||
|     pinned: Przypięty wpis | ||||
|  | ||||
| @ -21,7 +21,7 @@ module Mastodon | ||||
|     end | ||||
| 
 | ||||
|     def flags | ||||
|       'rc2' | ||||
|       'rc4' | ||||
|     end | ||||
| 
 | ||||
|     def to_a | ||||
|  | ||||
| @ -47,7 +47,7 @@ namespace :mastodon do | ||||
|     confirm = STDIN.gets.chomp | ||||
|     puts | ||||
| 
 | ||||
|     if confirm.casecmp?('y') | ||||
|     if confirm.casecmp('y').zero? | ||||
|       password = SecureRandom.hex | ||||
|       user = User.new(email: email, password: password, account_attributes: { username: username }) | ||||
|       if user.save | ||||
| @ -289,13 +289,13 @@ namespace :mastodon do | ||||
|       puts 'Delete records and associated files from deprecated preview cards? [y/N]: ' | ||||
|       confirm = STDIN.gets.chomp | ||||
| 
 | ||||
|       if confirm.casecmp?('y') | ||||
|       if confirm.casecmp('y').zero? | ||||
|         DeprecatedPreviewCard.in_batches.destroy_all | ||||
| 
 | ||||
|         puts 'Drop deprecated preview cards table? [y/N]: ' | ||||
|         confirm = STDIN.gets.chomp | ||||
| 
 | ||||
|         if confirm.casecmp?('y') | ||||
|         if confirm.casecmp('y').zero? | ||||
|           ActiveRecord::Migration.drop_table :deprecated_preview_cards | ||||
|         end | ||||
|       end | ||||
|  | ||||
							
								
								
									
										43
									
								
								public/embed.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								public/embed.js
									
									
									
									
									
										Normal 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(); | ||||
|     }); | ||||
|   }); | ||||
| })(); | ||||
| @ -4,10 +4,10 @@ require 'rails_helper' | ||||
| 
 | ||||
| describe SettingsHelper 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 | ||||
| 
 | ||||
|       expect(described_class::HUMAN_LOCALES.keys).to eq(options) | ||||
|       expect(described_class::HUMAN_LOCALES.keys).to include(*options) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|  | ||||
| @ -4,6 +4,10 @@ RSpec.describe ActivityPub::Activity::Update do | ||||
|   let!(:sender) { Fabricate(:account) } | ||||
| 
 | ||||
|   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)) | ||||
|   end | ||||
| 
 | ||||
|  | ||||
| @ -91,9 +91,35 @@ RSpec.describe ActivityPub::TagManager do | ||||
|   end | ||||
| 
 | ||||
|   describe '#uri_to_resource' do | ||||
|     it 'returns the local resource' do | ||||
|     it 'returns the local account' do | ||||
|       account = Fabricate(:account) | ||||
|       expect(subject.uri_to_resource(subject.uri_for(account), Account)).to eq account | ||||
|     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 | ||||
|  | ||||
| @ -21,4 +21,18 @@ describe Report do | ||||
|       expect(report.media_attachments).to eq [media_attachment] | ||||
|     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 | ||||
|  | ||||
| @ -26,7 +26,7 @@ RSpec.describe UnsubscribeService do | ||||
|     stub_request(:post, 'http://hub.example.com/').to_raise(HTTP::Error) | ||||
|     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 | ||||
| 
 | ||||
|   def stub_logger | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user