Merge branch 'master' into glitch-soc/merge-upstream
This commit is contained in:
		
						commit
						303aa05c63
					
				
					 23 changed files with 235 additions and 212 deletions
				
			
		|  | @ -82,7 +82,7 @@ class AccountsController < ApplicationController | |||
|   end | ||||
| 
 | ||||
|   def account_media_status_ids | ||||
|     @account.media_attachments.attached.reorder(nil).select(:status_id).distinct | ||||
|     @account.media_attachments.attached.reorder(nil).select(:status_id).group(:status_id) | ||||
|   end | ||||
| 
 | ||||
|   def no_replies_scope | ||||
|  |  | |||
|  | @ -14,7 +14,7 @@ module Admin | |||
|       @statuses = @account.statuses.where(visibility: [:public, :unlisted]) | ||||
| 
 | ||||
|       if params[:media] | ||||
|         account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).distinct | ||||
|         account_media_status_ids = @account.media_attachments.attached.reorder(nil).select(:status_id).group(:status_id) | ||||
|         @statuses.merge!(Status.where(id: account_media_status_ids)) | ||||
|       end | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,7 +1,6 @@ | |||
| import api from '../api'; | ||||
| import { debounce } from 'lodash'; | ||||
| import compareId from '../compare_id'; | ||||
| import { showAlertForError } from './alerts'; | ||||
| 
 | ||||
| export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST'; | ||||
| export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS'; | ||||
|  | @ -29,15 +28,19 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => { | |||
|       }, | ||||
|       body: JSON.stringify(params), | ||||
|     }); | ||||
| 
 | ||||
|     return; | ||||
|   } else if (navigator && navigator.sendBeacon) { | ||||
|     // Failing that, we can use sendBeacon, but we have to encode the data as
 | ||||
|     // FormData for DoorKeeper to recognize the token.
 | ||||
|     const formData = new FormData(); | ||||
| 
 | ||||
|     formData.append('bearer_token', accessToken); | ||||
| 
 | ||||
|     for (const [id, value] of Object.entries(params)) { | ||||
|       formData.append(`${id}[last_read_id]`, value.last_read_id); | ||||
|     } | ||||
| 
 | ||||
|     if (navigator.sendBeacon('/api/v1/markers', formData)) { | ||||
|       return; | ||||
|     } | ||||
|  | @ -85,11 +88,9 @@ const debouncedSubmitMarkers = debounce((dispatch, getState) => { | |||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   api().post('/api/v1/markers', params).then(() => { | ||||
|   api(getState).post('/api/v1/markers', params).then(() => { | ||||
|     dispatch(submitMarkersSuccess(params)); | ||||
|   }).catch(error => { | ||||
|     dispatch(showAlertForError(error)); | ||||
|   }); | ||||
|   }).catch(() => {}); | ||||
| }, 300000, { leading: true, trailing: true }); | ||||
| 
 | ||||
| export function submitMarkersSuccess({ home, notifications }) { | ||||
|  | @ -102,9 +103,11 @@ export function submitMarkersSuccess({ home, notifications }) { | |||
| 
 | ||||
| export function submitMarkers(params = {}) { | ||||
|   const result = (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState); | ||||
| 
 | ||||
|   if (params.immediate === true) { | ||||
|     debouncedSubmitMarkers.flush(); | ||||
|   } | ||||
| 
 | ||||
|   return result; | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										112
									
								
								app/javascript/mastodon/blurhash.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								app/javascript/mastodon/blurhash.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,112 @@ | |||
| const DIGIT_CHARACTERS = [ | ||||
|   '0', | ||||
|   '1', | ||||
|   '2', | ||||
|   '3', | ||||
|   '4', | ||||
|   '5', | ||||
|   '6', | ||||
|   '7', | ||||
|   '8', | ||||
|   '9', | ||||
|   'A', | ||||
|   'B', | ||||
|   'C', | ||||
|   'D', | ||||
|   'E', | ||||
|   'F', | ||||
|   'G', | ||||
|   'H', | ||||
|   'I', | ||||
|   'J', | ||||
|   'K', | ||||
|   'L', | ||||
|   'M', | ||||
|   'N', | ||||
|   'O', | ||||
|   'P', | ||||
|   'Q', | ||||
|   'R', | ||||
|   'S', | ||||
|   'T', | ||||
|   'U', | ||||
|   'V', | ||||
|   'W', | ||||
|   'X', | ||||
|   'Y', | ||||
|   'Z', | ||||
|   'a', | ||||
|   'b', | ||||
|   'c', | ||||
|   'd', | ||||
|   'e', | ||||
|   'f', | ||||
|   'g', | ||||
|   'h', | ||||
|   'i', | ||||
|   'j', | ||||
|   'k', | ||||
|   'l', | ||||
|   'm', | ||||
|   'n', | ||||
|   'o', | ||||
|   'p', | ||||
|   'q', | ||||
|   'r', | ||||
|   's', | ||||
|   't', | ||||
|   'u', | ||||
|   'v', | ||||
|   'w', | ||||
|   'x', | ||||
|   'y', | ||||
|   'z', | ||||
|   '#', | ||||
|   '$', | ||||
|   '%', | ||||
|   '*', | ||||
|   '+', | ||||
|   ',', | ||||
|   '-', | ||||
|   '.', | ||||
|   ':', | ||||
|   ';', | ||||
|   '=', | ||||
|   '?', | ||||
|   '@', | ||||
|   '[', | ||||
|   ']', | ||||
|   '^', | ||||
|   '_', | ||||
|   '{', | ||||
|   '|', | ||||
|   '}', | ||||
|   '~', | ||||
| ]; | ||||
| 
 | ||||
| export const decode83 = (str) => { | ||||
|   let value = 0; | ||||
|   let c, digit; | ||||
| 
 | ||||
|   for (let i = 0; i < str.length; i++) { | ||||
|     c = str[i]; | ||||
|     digit = DIGIT_CHARACTERS.indexOf(c); | ||||
|     value = value * 83 + digit; | ||||
|   } | ||||
| 
 | ||||
|   return value; | ||||
| }; | ||||
| 
 | ||||
| export const intToRGB = int => ({ | ||||
|   r: Math.max(0, (int >> 16)), | ||||
|   g: Math.max(0, (int >> 8) & 255), | ||||
|   b: Math.max(0, (int & 255)), | ||||
| }); | ||||
| 
 | ||||
| export const getAverageFromBlurhash = blurhash => { | ||||
|   if (!blurhash) { | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   return intToRGB(decode83(blurhash.slice(2, 6))); | ||||
| }; | ||||
|  | @ -1,7 +1,7 @@ | |||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import 'wicg-inert'; | ||||
| import { normal } from 'color-blend'; | ||||
| import { multiply } from 'color-blend'; | ||||
| 
 | ||||
| export default class ModalRoot extends React.PureComponent { | ||||
| 
 | ||||
|  | @ -98,7 +98,7 @@ export default class ModalRoot extends React.PureComponent { | |||
|     let backgroundColor = null; | ||||
| 
 | ||||
|     if (this.props.backgroundColor) { | ||||
|       backgroundColor = normal({ ...this.props.backgroundColor, a: 1 }, { r: 0, g: 0, b: 0, a: 0.3 }); | ||||
|       backgroundColor = multiply({ ...this.props.backgroundColor, a: 1 }, { r: 0, g: 0, b: 0, a: 0.7 }); | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|  |  | |||
|  | @ -97,7 +97,7 @@ class Status extends ImmutablePureComponent { | |||
|     cachedMediaWidth: PropTypes.number, | ||||
|     scrollKey: PropTypes.string, | ||||
|     deployPictureInPicture: PropTypes.func, | ||||
|     pictureInPicture: PropTypes.shape({ | ||||
|     pictureInPicture: ImmutablePropTypes.contains({ | ||||
|       inUse: PropTypes.bool, | ||||
|       available: PropTypes.bool, | ||||
|     }), | ||||
|  | @ -192,8 +192,9 @@ class Status extends ImmutablePureComponent { | |||
|     return <div className='audio-player' style={{ height: '110px' }} />; | ||||
|   } | ||||
| 
 | ||||
|   handleOpenVideo = (media, options) => { | ||||
|     this.props.onOpenVideo(this._properStatus().get('id'), media, options); | ||||
|   handleOpenVideo = (options) => { | ||||
|     const status = this._properStatus(); | ||||
|     this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), options); | ||||
|   } | ||||
| 
 | ||||
|   handleOpenMedia = (media, index) => { | ||||
|  | @ -202,15 +203,15 @@ class Status extends ImmutablePureComponent { | |||
| 
 | ||||
|   handleHotkeyOpenMedia = e => { | ||||
|     const { onOpenMedia, onOpenVideo } = this.props; | ||||
|     const statusId = this._properStatus().get('id'); | ||||
|     const status = this._properStatus(); | ||||
| 
 | ||||
|     e.preventDefault(); | ||||
| 
 | ||||
|     if (status.get('media_attachments').size > 0) { | ||||
|       if (status.getIn(['media_attachments', 0, 'type']) === 'video') { | ||||
|         onOpenVideo(statusId, status.getIn(['media_attachments', 0]), { startTime: 0 }); | ||||
|         onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), { startTime: 0 }); | ||||
|       } else { | ||||
|         onOpenMedia(statusId, status.get('media_attachments'), 0); | ||||
|         onOpenMedia(status.get('id'), status.get('media_attachments'), 0); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | @ -353,7 +354,7 @@ class Status extends ImmutablePureComponent { | |||
|       status  = status.get('reblog'); | ||||
|     } | ||||
| 
 | ||||
|     if (pictureInPicture.inUse) { | ||||
|     if (pictureInPicture.get('inUse')) { | ||||
|       media = <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />; | ||||
|     } else if (status.get('media_attachments').size > 0) { | ||||
|       if (this.props.muted) { | ||||
|  | @ -380,7 +381,7 @@ class Status extends ImmutablePureComponent { | |||
|                 width={this.props.cachedMediaWidth} | ||||
|                 height={110} | ||||
|                 cacheWidth={this.props.cacheMediaWidth} | ||||
|                 deployPictureInPicture={pictureInPicture.available ? this.handleDeployPictureInPicture : undefined} | ||||
|                 deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined} | ||||
|               /> | ||||
|             )} | ||||
|           </Bundle> | ||||
|  | @ -403,7 +404,7 @@ class Status extends ImmutablePureComponent { | |||
|                 sensitive={status.get('sensitive')} | ||||
|                 onOpenVideo={this.handleOpenVideo} | ||||
|                 cacheWidth={this.props.cacheMediaWidth} | ||||
|                 deployPictureInPicture={pictureInPicture.available ? this.handleDeployPictureInPicture : undefined} | ||||
|                 deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined} | ||||
|                 visible={this.state.showMedia} | ||||
|                 onToggleVisibility={this.handleToggleMediaVisibility} | ||||
|               /> | ||||
|  | @ -431,7 +432,7 @@ class Status extends ImmutablePureComponent { | |||
|     } else if (status.get('spoiler_text').length === 0 && status.get('card')) { | ||||
|       media = ( | ||||
|         <Card | ||||
|           onOpenMedia={this.props.onOpenMedia} | ||||
|           onOpenMedia={this.handleOpenMedia} | ||||
|           card={status.get('card')} | ||||
|           compact | ||||
|           cacheWidth={this.props.cacheMediaWidth} | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import React from 'react'; | ||||
| import { connect } from 'react-redux'; | ||||
| import Status from '../components/status'; | ||||
| import { makeGetStatus } from '../selectors'; | ||||
| import { makeGetStatus, makeGetPictureInPicture } from '../selectors'; | ||||
| import { | ||||
|   replyCompose, | ||||
|   mentionCompose, | ||||
|  | @ -54,14 +54,11 @@ const messages = defineMessages({ | |||
| 
 | ||||
| const makeMapStateToProps = () => { | ||||
|   const getStatus = makeGetStatus(); | ||||
|   const getPictureInPicture = makeGetPictureInPicture(); | ||||
| 
 | ||||
|   const mapStateToProps = (state, props) => ({ | ||||
|     status: getStatus(state, props), | ||||
| 
 | ||||
|     pictureInPicture: { | ||||
|       inUse: state.getIn(['meta', 'layout']) !== 'mobile' && state.get('picture_in_picture').statusId === props.id, | ||||
|       available: state.getIn(['meta', 'layout']) !== 'mobile', | ||||
|     }, | ||||
|     pictureInPicture: getPictureInPicture(state, props), | ||||
|   }); | ||||
| 
 | ||||
|   return mapStateToProps; | ||||
|  |  | |||
|  | @ -20,9 +20,9 @@ import RadioButton from 'mastodon/components/radio_button'; | |||
| const messages = defineMessages({ | ||||
|   deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' }, | ||||
|   deleteConfirm: { id: 'confirmations.delete_list.confirm', defaultMessage: 'Delete' }, | ||||
|   all_replies:   { id: 'lists.replies_policy.all_replies', defaultMessage: 'Any followed user' }, | ||||
|   no_replies:    { id: 'lists.replies_policy.no_replies', defaultMessage: 'No one' }, | ||||
|   list_replies:  { id: 'lists.replies_policy.list_replies', defaultMessage: 'Members of the list' }, | ||||
|   followed:   { id: 'lists.replies_policy.followed', defaultMessage: 'Any followed user' }, | ||||
|   none:    { id: 'lists.replies_policy.none', defaultMessage: 'No one' }, | ||||
|   list:  { id: 'lists.replies_policy.list', defaultMessage: 'Members of the list' }, | ||||
| }); | ||||
| 
 | ||||
| const mapStateToProps = (state, props) => ({ | ||||
|  | @ -193,7 +193,7 @@ class ListTimeline extends React.PureComponent { | |||
|                 <FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' /> | ||||
|               </span> | ||||
|               <div className='column-settings__row'> | ||||
|                 { ['no_replies', 'list_replies', 'all_replies'].map(policy => ( | ||||
|                 { ['none', 'list', 'followed'].map(policy => ( | ||||
|                   <RadioButton name='order' value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} /> | ||||
|                 ))} | ||||
|               </div> | ||||
|  |  | |||
|  | @ -41,7 +41,10 @@ class DetailedStatus extends ImmutablePureComponent { | |||
|     domain: PropTypes.string.isRequired, | ||||
|     compact: PropTypes.bool, | ||||
|     showMedia: PropTypes.bool, | ||||
|     usingPiP: PropTypes.bool, | ||||
|     pictureInPicture: ImmutablePropTypes.contains({ | ||||
|       inUse: PropTypes.bool, | ||||
|       available: PropTypes.bool, | ||||
|     }), | ||||
|     onToggleMediaVisibility: PropTypes.func, | ||||
|   }; | ||||
| 
 | ||||
|  | @ -58,8 +61,8 @@ class DetailedStatus extends ImmutablePureComponent { | |||
|     e.stopPropagation(); | ||||
|   } | ||||
| 
 | ||||
|   handleOpenVideo = (media, options) => { | ||||
|     this.props.onOpenVideo(media, options); | ||||
|   handleOpenVideo = (options) => { | ||||
|     this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), options); | ||||
|   } | ||||
| 
 | ||||
|   handleExpandedToggle = () => { | ||||
|  | @ -102,7 +105,7 @@ class DetailedStatus extends ImmutablePureComponent { | |||
|   render () { | ||||
|     const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status; | ||||
|     const outerStyle = { boxSizing: 'border-box' }; | ||||
|     const { intl, compact, usingPiP } = this.props; | ||||
|     const { intl, compact, pictureInPicture } = this.props; | ||||
| 
 | ||||
|     if (!status) { | ||||
|       return null; | ||||
|  | @ -118,7 +121,7 @@ class DetailedStatus extends ImmutablePureComponent { | |||
|       outerStyle.height = `${this.state.height}px`; | ||||
|     } | ||||
| 
 | ||||
|     if (usingPiP) { | ||||
|     if (pictureInPicture.get('inUse')) { | ||||
|       media = <PictureInPicturePlaceholder />; | ||||
|     } else if (status.get('media_attachments').size > 0) { | ||||
|       if (status.getIn(['media_attachments', 0, 'type']) === 'audio') { | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import { connect } from 'react-redux'; | ||||
| import DetailedStatus from '../components/detailed_status'; | ||||
| import { makeGetStatus } from '../../../selectors'; | ||||
| import { makeGetStatus, makeGetPictureInPicture } from '../../../selectors'; | ||||
| import { | ||||
|   replyCompose, | ||||
|   mentionCompose, | ||||
|  | @ -40,10 +40,12 @@ const messages = defineMessages({ | |||
| 
 | ||||
| const makeMapStateToProps = () => { | ||||
|   const getStatus = makeGetStatus(); | ||||
|   const getPictureInPicture = makeGetPictureInPicture(); | ||||
| 
 | ||||
|   const mapStateToProps = (state, props) => ({ | ||||
|     status: getStatus(state, props), | ||||
|     domain: state.getIn(['meta', 'domain']), | ||||
|     pictureInPicture: getPictureInPicture(state, props), | ||||
|   }); | ||||
| 
 | ||||
|   return mapStateToProps; | ||||
|  |  | |||
|  | @ -43,7 +43,7 @@ import { | |||
| import { initMuteModal } from '../../actions/mutes'; | ||||
| import { initBlockModal } from '../../actions/blocks'; | ||||
| import { initReport } from '../../actions/reports'; | ||||
| import { makeGetStatus } from '../../selectors'; | ||||
| import { makeGetStatus, makeGetPictureInPicture } from '../../selectors'; | ||||
| import { ScrollContainer } from 'react-router-scroll-4'; | ||||
| import ColumnBackButton from '../../components/column_back_button'; | ||||
| import ColumnHeader from '../../components/column_header'; | ||||
|  | @ -72,6 +72,7 @@ const messages = defineMessages({ | |||
| 
 | ||||
| const makeMapStateToProps = () => { | ||||
|   const getStatus = makeGetStatus(); | ||||
|   const getPictureInPicture = makeGetPictureInPicture(); | ||||
| 
 | ||||
|   const getAncestorsIds = createSelector([ | ||||
|     (_, { id }) => id, | ||||
|  | @ -129,11 +130,12 @@ const makeMapStateToProps = () => { | |||
| 
 | ||||
|   const mapStateToProps = (state, props) => { | ||||
|     const status = getStatus(state, { id: props.params.statusId }); | ||||
|     let ancestorsIds = Immutable.List(); | ||||
| 
 | ||||
|     let ancestorsIds   = Immutable.List(); | ||||
|     let descendantsIds = Immutable.List(); | ||||
| 
 | ||||
|     if (status) { | ||||
|       ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') }); | ||||
|       ancestorsIds   = getAncestorsIds(state, { id: status.get('in_reply_to_id') }); | ||||
|       descendantsIds = getDescendantsIds(state, { id: status.get('id') }); | ||||
|     } | ||||
| 
 | ||||
|  | @ -143,7 +145,7 @@ const makeMapStateToProps = () => { | |||
|       descendantsIds, | ||||
|       askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0, | ||||
|       domain: state.getIn(['meta', 'domain']), | ||||
|       usingPiP: state.get('picture_in_picture').statusId === props.params.statusId, | ||||
|       pictureInPicture: getPictureInPicture(state, { id: props.params.statusId }), | ||||
|     }; | ||||
|   }; | ||||
| 
 | ||||
|  | @ -168,7 +170,10 @@ class Status extends ImmutablePureComponent { | |||
|     askReplyConfirmation: PropTypes.bool, | ||||
|     multiColumn: PropTypes.bool, | ||||
|     domain: PropTypes.string.isRequired, | ||||
|     usingPiP: PropTypes.bool, | ||||
|     pictureInPicture: ImmutablePropTypes.contains({ | ||||
|       inUse: PropTypes.bool, | ||||
|       available: PropTypes.bool, | ||||
|     }), | ||||
|   }; | ||||
| 
 | ||||
|   state = { | ||||
|  | @ -492,7 +497,7 @@ class Status extends ImmutablePureComponent { | |||
| 
 | ||||
|   render () { | ||||
|     let ancestors, descendants; | ||||
|     const { shouldUpdateScroll, status, ancestorsIds, descendantsIds, intl, domain, multiColumn, usingPiP } = this.props; | ||||
|     const { shouldUpdateScroll, status, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props; | ||||
|     const { fullscreen } = this.state; | ||||
| 
 | ||||
|     if (status === null) { | ||||
|  | @ -550,7 +555,7 @@ class Status extends ImmutablePureComponent { | |||
|                   domain={domain} | ||||
|                   showMedia={this.state.showMedia} | ||||
|                   onToggleMediaVisibility={this.handleToggleMediaVisibility} | ||||
|                   usingPiP={usingPiP} | ||||
|                   pictureInPicture={pictureInPicture} | ||||
|                 /> | ||||
| 
 | ||||
|                 <ActionBar | ||||
|  |  | |||
|  | @ -4,13 +4,11 @@ import PropTypes from 'prop-types'; | |||
| import Audio from 'mastodon/features/audio'; | ||||
| import { connect } from 'react-redux'; | ||||
| import ImmutablePureComponent from 'react-immutable-pure-component'; | ||||
| import { FormattedMessage } from 'react-intl'; | ||||
| import { previewState } from './video_modal'; | ||||
| import classNames from 'classnames'; | ||||
| import Icon from 'mastodon/components/icon'; | ||||
| import Footer from 'mastodon/features/picture_in_picture/components/footer'; | ||||
| 
 | ||||
| const mapStateToProps = (state, { status }) => ({ | ||||
|   account: state.getIn(['accounts', status.get('account')]), | ||||
| const mapStateToProps = (state, { statusId }) => ({ | ||||
|   accountStaticAvatar: state.getIn(['accounts', state.getIn(['statuses', statusId, 'account']), 'avatar_static']), | ||||
| }); | ||||
| 
 | ||||
| export default @connect(mapStateToProps) | ||||
|  | @ -18,12 +16,13 @@ class AudioModal extends ImmutablePureComponent { | |||
| 
 | ||||
|   static propTypes = { | ||||
|     media: ImmutablePropTypes.map.isRequired, | ||||
|     status: ImmutablePropTypes.map, | ||||
|     statusId: PropTypes.string.isRequired, | ||||
|     accountStaticAvatar: PropTypes.string.isRequired, | ||||
|     options: PropTypes.shape({ | ||||
|       autoPlay: PropTypes.bool, | ||||
|     }), | ||||
|     account: ImmutablePropTypes.map, | ||||
|     onClose: PropTypes.func.isRequired, | ||||
|     onChangeBackgroundColor: PropTypes.func.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   static contextTypes = { | ||||
|  | @ -52,15 +51,8 @@ class AudioModal extends ImmutablePureComponent { | |||
|     } | ||||
|   } | ||||
| 
 | ||||
|   handleStatusClick = e => { | ||||
|     if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { | ||||
|       e.preventDefault(); | ||||
|       this.context.router.history.push(`/statuses/${this.props.status.get('id')}`); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { media, status, account } = this.props; | ||||
|     const { media, accountStaticAvatar, statusId, onClose } = this.props; | ||||
|     const options = this.props.options || {}; | ||||
| 
 | ||||
|     return ( | ||||
|  | @ -71,7 +63,7 @@ class AudioModal extends ImmutablePureComponent { | |||
|             alt={media.get('description')} | ||||
|             duration={media.getIn(['meta', 'original', 'duration'], 0)} | ||||
|             height={150} | ||||
|             poster={media.get('preview_url') || account.get('avatar_static')} | ||||
|             poster={media.get('preview_url') || accountStaticAvatar} | ||||
|             backgroundColor={media.getIn(['meta', 'colors', 'background'])} | ||||
|             foregroundColor={media.getIn(['meta', 'colors', 'foreground'])} | ||||
|             accentColor={media.getIn(['meta', 'colors', 'accent'])} | ||||
|  | @ -79,11 +71,9 @@ class AudioModal extends ImmutablePureComponent { | |||
|           /> | ||||
|         </div> | ||||
| 
 | ||||
|         {status && ( | ||||
|           <div className={classNames('media-modal__meta')}> | ||||
|             <a href={status.get('url')} onClick={this.handleStatusClick}><Icon id='comments' /> <FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a> | ||||
|           </div> | ||||
|         )} | ||||
|         <div className='media-modal__overlay'> | ||||
|           {statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />} | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|  |  | |||
|  | @ -75,7 +75,9 @@ class ColumnsArea extends ImmutablePureComponent { | |||
|   } | ||||
| 
 | ||||
|   componentWillReceiveProps() { | ||||
|     this.setState({ shouldAnimate: false }); | ||||
|     if (typeof this.pendingIndex !== 'number' && this.lastIndex !== getIndex(this.context.router.history.location.pathname)) { | ||||
|       this.setState({ shouldAnimate: false }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   componentDidMount() { | ||||
|  | @ -99,8 +101,13 @@ class ColumnsArea extends ImmutablePureComponent { | |||
|     if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) { | ||||
|       this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false); | ||||
|     } | ||||
|     this.lastIndex = getIndex(this.context.router.history.location.pathname); | ||||
|     this.setState({ shouldAnimate: true }); | ||||
| 
 | ||||
|     const newIndex = getIndex(this.context.router.history.location.pathname); | ||||
| 
 | ||||
|     if (this.lastIndex !== newIndex) { | ||||
|       this.lastIndex = newIndex; | ||||
|       this.setState({ shouldAnimate: true }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   componentWillUnmount () { | ||||
|  |  | |||
|  | @ -12,6 +12,7 @@ import Icon from 'mastodon/components/icon'; | |||
| import GIFV from 'mastodon/components/gifv'; | ||||
| import { disableSwiping } from 'mastodon/initial_state'; | ||||
| import Footer from 'mastodon/features/picture_in_picture/components/footer'; | ||||
| import { getAverageFromBlurhash } from 'mastodon/blurhash'; | ||||
| 
 | ||||
| const messages = defineMessages({ | ||||
|   close: { id: 'lightbox.close', defaultMessage: 'Close' }, | ||||
|  | @ -21,111 +22,6 @@ const messages = defineMessages({ | |||
| 
 | ||||
| export const previewState = 'previewMediaModal'; | ||||
| 
 | ||||
| const digitCharacters = [ | ||||
|   '0', | ||||
|   '1', | ||||
|   '2', | ||||
|   '3', | ||||
|   '4', | ||||
|   '5', | ||||
|   '6', | ||||
|   '7', | ||||
|   '8', | ||||
|   '9', | ||||
|   'A', | ||||
|   'B', | ||||
|   'C', | ||||
|   'D', | ||||
|   'E', | ||||
|   'F', | ||||
|   'G', | ||||
|   'H', | ||||
|   'I', | ||||
|   'J', | ||||
|   'K', | ||||
|   'L', | ||||
|   'M', | ||||
|   'N', | ||||
|   'O', | ||||
|   'P', | ||||
|   'Q', | ||||
|   'R', | ||||
|   'S', | ||||
|   'T', | ||||
|   'U', | ||||
|   'V', | ||||
|   'W', | ||||
|   'X', | ||||
|   'Y', | ||||
|   'Z', | ||||
|   'a', | ||||
|   'b', | ||||
|   'c', | ||||
|   'd', | ||||
|   'e', | ||||
|   'f', | ||||
|   'g', | ||||
|   'h', | ||||
|   'i', | ||||
|   'j', | ||||
|   'k', | ||||
|   'l', | ||||
|   'm', | ||||
|   'n', | ||||
|   'o', | ||||
|   'p', | ||||
|   'q', | ||||
|   'r', | ||||
|   's', | ||||
|   't', | ||||
|   'u', | ||||
|   'v', | ||||
|   'w', | ||||
|   'x', | ||||
|   'y', | ||||
|   'z', | ||||
|   '#', | ||||
|   '$', | ||||
|   '%', | ||||
|   '*', | ||||
|   '+', | ||||
|   ',', | ||||
|   '-', | ||||
|   '.', | ||||
|   ':', | ||||
|   ';', | ||||
|   '=', | ||||
|   '?', | ||||
|   '@', | ||||
|   '[', | ||||
|   ']', | ||||
|   '^', | ||||
|   '_', | ||||
|   '{', | ||||
|   '|', | ||||
|   '}', | ||||
|   '~', | ||||
| ]; | ||||
| 
 | ||||
| const decode83 = (str) => { | ||||
|   let value = 0; | ||||
|   let c, digit; | ||||
| 
 | ||||
|   for (let i = 0; i < str.length; i++) { | ||||
|     c = str[i]; | ||||
|     digit = digitCharacters.indexOf(c); | ||||
|     value = value * 83 + digit; | ||||
|   } | ||||
| 
 | ||||
|   return value; | ||||
| }; | ||||
| 
 | ||||
| const decodeRGB = int => ({ | ||||
|   r: Math.max(0, (int >> 16)), | ||||
|   g: Math.max(0, (int >> 8) & 255), | ||||
|   b: Math.max(0, (int & 255)), | ||||
| }); | ||||
| 
 | ||||
| export default @injectIntl | ||||
| class MediaModal extends ImmutablePureComponent { | ||||
| 
 | ||||
|  | @ -224,7 +120,7 @@ class MediaModal extends ImmutablePureComponent { | |||
|     const blurhash = media.getIn([index, 'blurhash']); | ||||
| 
 | ||||
|     if (blurhash) { | ||||
|       const backgroundColor = decodeRGB(decode83(blurhash.slice(2, 6))); | ||||
|       const backgroundColor = getAverageFromBlurhash(blurhash); | ||||
|       onChangeBackgroundColor(backgroundColor); | ||||
|     } | ||||
|   } | ||||
|  |  | |||
|  | @ -3,6 +3,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; | |||
| import PropTypes from 'prop-types'; | ||||
| import Video from 'mastodon/features/video'; | ||||
| import ImmutablePureComponent from 'react-immutable-pure-component'; | ||||
| import Footer from 'mastodon/features/picture_in_picture/components/footer'; | ||||
| import { getAverageFromBlurhash } from 'mastodon/blurhash'; | ||||
| 
 | ||||
| export const previewState = 'previewVideoModal'; | ||||
| 
 | ||||
|  | @ -17,6 +19,7 @@ export default class VideoModal extends ImmutablePureComponent { | |||
|       defaultVolume: PropTypes.number, | ||||
|     }), | ||||
|     onClose: PropTypes.func.isRequired, | ||||
|     onChangeBackgroundColor: PropTypes.func.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   static contextTypes = { | ||||
|  | @ -24,29 +27,35 @@ export default class VideoModal extends ImmutablePureComponent { | |||
|   }; | ||||
| 
 | ||||
|   componentDidMount () { | ||||
|     if (this.context.router) { | ||||
|       const history = this.context.router.history; | ||||
|     const { router } = this.context; | ||||
|     const { media, onChangeBackgroundColor, onClose } = this.props; | ||||
| 
 | ||||
|       history.push(history.location.pathname, previewState); | ||||
|     if (router) { | ||||
|       router.history.push(router.history.location.pathname, previewState); | ||||
|       this.unlistenHistory = router.history.listen(() => onClose()); | ||||
|     } | ||||
| 
 | ||||
|       this.unlistenHistory = history.listen(() => { | ||||
|         this.props.onClose(); | ||||
|       }); | ||||
|     const backgroundColor = getAverageFromBlurhash(media.get('blurhash')); | ||||
| 
 | ||||
|     if (backgroundColor) { | ||||
|       onChangeBackgroundColor(backgroundColor); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   componentWillUnmount () { | ||||
|     if (this.context.router) { | ||||
|     const { router } = this.context; | ||||
| 
 | ||||
|     if (router) { | ||||
|       this.unlistenHistory(); | ||||
| 
 | ||||
|       if (this.context.router.history.location.state === previewState) { | ||||
|         this.context.router.history.goBack(); | ||||
|       if (router.history.location.state === previewState) { | ||||
|         router.history.goBack(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   render () { | ||||
|     const { media, onClose } = this.props; | ||||
|     const { media, statusId, onClose } = this.props; | ||||
|     const options = this.props.options || {}; | ||||
| 
 | ||||
|     return ( | ||||
|  | @ -65,6 +74,10 @@ export default class VideoModal extends ImmutablePureComponent { | |||
|             alt={media.get('description')} | ||||
|           /> | ||||
|         </div> | ||||
| 
 | ||||
|         <div className='media-modal__overlay'> | ||||
|           {statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />} | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | ||||
| import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; | ||||
| import { fromJS, is } from 'immutable'; | ||||
| import { is } from 'immutable'; | ||||
| import { throttle, debounce } from 'lodash'; | ||||
| import classNames from 'classnames'; | ||||
| import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen'; | ||||
|  | @ -495,25 +495,13 @@ class Video extends React.PureComponent { | |||
|   } | ||||
| 
 | ||||
|   handleOpenVideo = () => { | ||||
|     const { src, preview, width, height, alt } = this.props; | ||||
|     this.video.pause(); | ||||
| 
 | ||||
|     const media = fromJS({ | ||||
|       type: 'video', | ||||
|       url: src, | ||||
|       preview_url: preview, | ||||
|       description: alt, | ||||
|       width, | ||||
|       height, | ||||
|     }); | ||||
| 
 | ||||
|     const options = { | ||||
|     this.props.onOpenVideo({ | ||||
|       startTime: this.video.currentTime, | ||||
|       autoPlay: !this.state.paused, | ||||
|       defaultVolume: this.state.volume, | ||||
|     }; | ||||
| 
 | ||||
|     this.video.pause(); | ||||
|     this.props.onOpenVideo(media, options); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   handleCloseVideo = () => { | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import { createSelector } from 'reselect'; | ||||
| import { List as ImmutableList, is } from 'immutable'; | ||||
| import { List as ImmutableList, Map as ImmutableMap, is } from 'immutable'; | ||||
| import { me } from '../initial_state'; | ||||
| 
 | ||||
| const getAccountBase         = (state, id) => state.getIn(['accounts', id], null); | ||||
|  | @ -121,6 +121,16 @@ export const makeGetStatus = () => { | |||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export const makeGetPictureInPicture = () => { | ||||
|   return createSelector([ | ||||
|     (state, { id }) => state.get('picture_in_picture').statusId === id, | ||||
|     (state) => state.getIn(['meta', 'layout']) !== 'mobile', | ||||
|   ], (inUse, available) => ImmutableMap({ | ||||
|     inUse: inUse && available, | ||||
|     available, | ||||
|   })); | ||||
| }; | ||||
| 
 | ||||
| const getAlertsBase = state => state.get('alerts'); | ||||
| 
 | ||||
| export const getAlerts = createSelector([getAlertsBase], (base) => { | ||||
|  |  | |||
|  | @ -403,8 +403,8 @@ class FeedManager | |||
|   def filter_from_list?(status, list) | ||||
|     if status.reply? && status.in_reply_to_account_id != status.account_id | ||||
|       should_filter = status.in_reply_to_account_id != list.account_id | ||||
|       should_filter &&= !list.show_all_replies? | ||||
|       should_filter &&= !(list.show_list_replies? && ListAccount.where(list_id: list.id, account_id: status.in_reply_to_account_id).exists?) | ||||
|       should_filter &&= !list.show_followed? | ||||
|       should_filter &&= !(list.show_list? && ListAccount.where(list_id: list.id, account_id: status.in_reply_to_account_id).exists?) | ||||
| 
 | ||||
|       return !!should_filter | ||||
|     end | ||||
|  |  | |||
|  | @ -445,7 +445,7 @@ class Account < ApplicationRecord | |||
|     end | ||||
| 
 | ||||
|     def inboxes | ||||
|       urls = reorder(nil).where(protocol: :activitypub).pluck(Arel.sql("distinct coalesce(nullif(accounts.shared_inbox_url, ''), accounts.inbox_url)")) | ||||
|       urls = reorder(nil).where(protocol: :activitypub).group(:preferred_inbox_url).pluck(Arel.sql("coalesce(nullif(accounts.shared_inbox_url, ''), accounts.inbox_url) AS preferred_inbox_url")) | ||||
|       DeliveryFailureTracker.without_unavailable(urls) | ||||
|     end | ||||
| 
 | ||||
|  |  | |||
|  | @ -51,7 +51,7 @@ class Form::AccountBatch | |||
|   end | ||||
| 
 | ||||
|   def account_domains | ||||
|     accounts.pluck(Arel.sql('distinct domain')).compact | ||||
|     accounts.group(:domain).pluck(:domain).compact | ||||
|   end | ||||
| 
 | ||||
|   def accounts | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ | |||
| #  title          :string           default(""), not null | ||||
| #  created_at     :datetime         not null | ||||
| #  updated_at     :datetime         not null | ||||
| #  replies_policy :integer          default("list_replies"), not null | ||||
| #  replies_policy :integer          default("list"), not null | ||||
| # | ||||
| 
 | ||||
| class List < ApplicationRecord | ||||
|  | @ -16,7 +16,7 @@ class List < ApplicationRecord | |||
| 
 | ||||
|   PER_ACCOUNT_LIMIT = 50 | ||||
| 
 | ||||
|   enum replies_policy: [:list_replies, :all_replies, :no_replies], _prefix: :show | ||||
|   enum replies_policy: [:list, :followed, :none], _prefix: :show | ||||
| 
 | ||||
|   belongs_to :account, optional: true | ||||
| 
 | ||||
|  |  | |||
|  | @ -9,10 +9,6 @@ class REST::AccountFeaturedTagSerializer < ActiveModel::Serializer | |||
|     object.tag.id.to_s | ||||
|   end | ||||
| 
 | ||||
|   def name | ||||
|     "##{object.name}" | ||||
|   end | ||||
| 
 | ||||
|   def url | ||||
|     short_account_tag_url(object.account, object.tag) | ||||
|   end | ||||
|  |  | |||
|  | @ -342,7 +342,7 @@ RSpec.describe FeedManager do | |||
| 
 | ||||
|     context 'when replies policy is set to no replies' do | ||||
|       before do | ||||
|         list.replies_policy = :no_replies | ||||
|         list.replies_policy = :none | ||||
|       end | ||||
| 
 | ||||
|       it 'pushes statuses that are not replies' do | ||||
|  | @ -365,7 +365,7 @@ RSpec.describe FeedManager do | |||
| 
 | ||||
|     context 'when replies policy is set to list-only replies' do | ||||
|       before do | ||||
|         list.replies_policy = :list_replies | ||||
|         list.replies_policy = :list | ||||
|       end | ||||
| 
 | ||||
|       it 'pushes statuses that are not replies' do | ||||
|  | @ -394,7 +394,7 @@ RSpec.describe FeedManager do | |||
| 
 | ||||
|     context 'when replies policy is set to any reply' do | ||||
|       before do | ||||
|         list.replies_policy = :all_replies | ||||
|         list.replies_policy = :followed | ||||
|       end | ||||
| 
 | ||||
|       it 'pushes statuses that are not replies' do | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue