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 |   end | ||||||
| 
 | 
 | ||||||
|   def account_media_status_ids |   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 |   end | ||||||
| 
 | 
 | ||||||
|   def no_replies_scope |   def no_replies_scope | ||||||
|  |  | ||||||
|  | @ -14,7 +14,7 @@ module Admin | ||||||
|       @statuses = @account.statuses.where(visibility: [:public, :unlisted]) |       @statuses = @account.statuses.where(visibility: [:public, :unlisted]) | ||||||
| 
 | 
 | ||||||
|       if params[:media] |       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)) |         @statuses.merge!(Status.where(id: account_media_status_ids)) | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,7 +1,6 @@ | ||||||
| import api from '../api'; | import api from '../api'; | ||||||
| import { debounce } from 'lodash'; | import { debounce } from 'lodash'; | ||||||
| import compareId from '../compare_id'; | import compareId from '../compare_id'; | ||||||
| import { showAlertForError } from './alerts'; |  | ||||||
| 
 | 
 | ||||||
| export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST'; | export const MARKERS_FETCH_REQUEST = 'MARKERS_FETCH_REQUEST'; | ||||||
| export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS'; | export const MARKERS_FETCH_SUCCESS = 'MARKERS_FETCH_SUCCESS'; | ||||||
|  | @ -29,15 +28,19 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => { | ||||||
|       }, |       }, | ||||||
|       body: JSON.stringify(params), |       body: JSON.stringify(params), | ||||||
|     }); |     }); | ||||||
|  | 
 | ||||||
|     return; |     return; | ||||||
|   } else if (navigator && navigator.sendBeacon) { |   } else if (navigator && navigator.sendBeacon) { | ||||||
|     // Failing that, we can use sendBeacon, but we have to encode the data as
 |     // Failing that, we can use sendBeacon, but we have to encode the data as
 | ||||||
|     // FormData for DoorKeeper to recognize the token.
 |     // FormData for DoorKeeper to recognize the token.
 | ||||||
|     const formData = new FormData(); |     const formData = new FormData(); | ||||||
|  | 
 | ||||||
|     formData.append('bearer_token', accessToken); |     formData.append('bearer_token', accessToken); | ||||||
|  | 
 | ||||||
|     for (const [id, value] of Object.entries(params)) { |     for (const [id, value] of Object.entries(params)) { | ||||||
|       formData.append(`${id}[last_read_id]`, value.last_read_id); |       formData.append(`${id}[last_read_id]`, value.last_read_id); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     if (navigator.sendBeacon('/api/v1/markers', formData)) { |     if (navigator.sendBeacon('/api/v1/markers', formData)) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  | @ -85,11 +88,9 @@ const debouncedSubmitMarkers = debounce((dispatch, getState) => { | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   api().post('/api/v1/markers', params).then(() => { |   api(getState).post('/api/v1/markers', params).then(() => { | ||||||
|     dispatch(submitMarkersSuccess(params)); |     dispatch(submitMarkersSuccess(params)); | ||||||
|   }).catch(error => { |   }).catch(() => {}); | ||||||
|     dispatch(showAlertForError(error)); |  | ||||||
|   }); |  | ||||||
| }, 300000, { leading: true, trailing: true }); | }, 300000, { leading: true, trailing: true }); | ||||||
| 
 | 
 | ||||||
| export function submitMarkersSuccess({ home, notifications }) { | export function submitMarkersSuccess({ home, notifications }) { | ||||||
|  | @ -102,9 +103,11 @@ export function submitMarkersSuccess({ home, notifications }) { | ||||||
| 
 | 
 | ||||||
| export function submitMarkers(params = {}) { | export function submitMarkers(params = {}) { | ||||||
|   const result = (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState); |   const result = (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState); | ||||||
|  | 
 | ||||||
|   if (params.immediate === true) { |   if (params.immediate === true) { | ||||||
|     debouncedSubmitMarkers.flush(); |     debouncedSubmitMarkers.flush(); | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|   return result; |   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 React from 'react'; | ||||||
| import PropTypes from 'prop-types'; | import PropTypes from 'prop-types'; | ||||||
| import 'wicg-inert'; | import 'wicg-inert'; | ||||||
| import { normal } from 'color-blend'; | import { multiply } from 'color-blend'; | ||||||
| 
 | 
 | ||||||
| export default class ModalRoot extends React.PureComponent { | export default class ModalRoot extends React.PureComponent { | ||||||
| 
 | 
 | ||||||
|  | @ -98,7 +98,7 @@ export default class ModalRoot extends React.PureComponent { | ||||||
|     let backgroundColor = null; |     let backgroundColor = null; | ||||||
| 
 | 
 | ||||||
|     if (this.props.backgroundColor) { |     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 ( |     return ( | ||||||
|  |  | ||||||
|  | @ -97,7 +97,7 @@ class Status extends ImmutablePureComponent { | ||||||
|     cachedMediaWidth: PropTypes.number, |     cachedMediaWidth: PropTypes.number, | ||||||
|     scrollKey: PropTypes.string, |     scrollKey: PropTypes.string, | ||||||
|     deployPictureInPicture: PropTypes.func, |     deployPictureInPicture: PropTypes.func, | ||||||
|     pictureInPicture: PropTypes.shape({ |     pictureInPicture: ImmutablePropTypes.contains({ | ||||||
|       inUse: PropTypes.bool, |       inUse: PropTypes.bool, | ||||||
|       available: PropTypes.bool, |       available: PropTypes.bool, | ||||||
|     }), |     }), | ||||||
|  | @ -192,8 +192,9 @@ class Status extends ImmutablePureComponent { | ||||||
|     return <div className='audio-player' style={{ height: '110px' }} />; |     return <div className='audio-player' style={{ height: '110px' }} />; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   handleOpenVideo = (media, options) => { |   handleOpenVideo = (options) => { | ||||||
|     this.props.onOpenVideo(this._properStatus().get('id'), media, options); |     const status = this._properStatus(); | ||||||
|  |     this.props.onOpenVideo(status.get('id'), status.getIn(['media_attachments', 0]), options); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   handleOpenMedia = (media, index) => { |   handleOpenMedia = (media, index) => { | ||||||
|  | @ -202,15 +203,15 @@ class Status extends ImmutablePureComponent { | ||||||
| 
 | 
 | ||||||
|   handleHotkeyOpenMedia = e => { |   handleHotkeyOpenMedia = e => { | ||||||
|     const { onOpenMedia, onOpenVideo } = this.props; |     const { onOpenMedia, onOpenVideo } = this.props; | ||||||
|     const statusId = this._properStatus().get('id'); |     const status = this._properStatus(); | ||||||
| 
 | 
 | ||||||
|     e.preventDefault(); |     e.preventDefault(); | ||||||
| 
 | 
 | ||||||
|     if (status.get('media_attachments').size > 0) { |     if (status.get('media_attachments').size > 0) { | ||||||
|       if (status.getIn(['media_attachments', 0, 'type']) === 'video') { |       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 { |       } 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'); |       status  = status.get('reblog'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (pictureInPicture.inUse) { |     if (pictureInPicture.get('inUse')) { | ||||||
|       media = <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />; |       media = <PictureInPicturePlaceholder width={this.props.cachedMediaWidth} />; | ||||||
|     } else if (status.get('media_attachments').size > 0) { |     } else if (status.get('media_attachments').size > 0) { | ||||||
|       if (this.props.muted) { |       if (this.props.muted) { | ||||||
|  | @ -380,7 +381,7 @@ class Status extends ImmutablePureComponent { | ||||||
|                 width={this.props.cachedMediaWidth} |                 width={this.props.cachedMediaWidth} | ||||||
|                 height={110} |                 height={110} | ||||||
|                 cacheWidth={this.props.cacheMediaWidth} |                 cacheWidth={this.props.cacheMediaWidth} | ||||||
|                 deployPictureInPicture={pictureInPicture.available ? this.handleDeployPictureInPicture : undefined} |                 deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined} | ||||||
|               /> |               /> | ||||||
|             )} |             )} | ||||||
|           </Bundle> |           </Bundle> | ||||||
|  | @ -403,7 +404,7 @@ class Status extends ImmutablePureComponent { | ||||||
|                 sensitive={status.get('sensitive')} |                 sensitive={status.get('sensitive')} | ||||||
|                 onOpenVideo={this.handleOpenVideo} |                 onOpenVideo={this.handleOpenVideo} | ||||||
|                 cacheWidth={this.props.cacheMediaWidth} |                 cacheWidth={this.props.cacheMediaWidth} | ||||||
|                 deployPictureInPicture={pictureInPicture.available ? this.handleDeployPictureInPicture : undefined} |                 deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined} | ||||||
|                 visible={this.state.showMedia} |                 visible={this.state.showMedia} | ||||||
|                 onToggleVisibility={this.handleToggleMediaVisibility} |                 onToggleVisibility={this.handleToggleMediaVisibility} | ||||||
|               /> |               /> | ||||||
|  | @ -431,7 +432,7 @@ class Status extends ImmutablePureComponent { | ||||||
|     } else if (status.get('spoiler_text').length === 0 && status.get('card')) { |     } else if (status.get('spoiler_text').length === 0 && status.get('card')) { | ||||||
|       media = ( |       media = ( | ||||||
|         <Card |         <Card | ||||||
|           onOpenMedia={this.props.onOpenMedia} |           onOpenMedia={this.handleOpenMedia} | ||||||
|           card={status.get('card')} |           card={status.get('card')} | ||||||
|           compact |           compact | ||||||
|           cacheWidth={this.props.cacheMediaWidth} |           cacheWidth={this.props.cacheMediaWidth} | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import React from 'react'; | import React from 'react'; | ||||||
| import { connect } from 'react-redux'; | import { connect } from 'react-redux'; | ||||||
| import Status from '../components/status'; | import Status from '../components/status'; | ||||||
| import { makeGetStatus } from '../selectors'; | import { makeGetStatus, makeGetPictureInPicture } from '../selectors'; | ||||||
| import { | import { | ||||||
|   replyCompose, |   replyCompose, | ||||||
|   mentionCompose, |   mentionCompose, | ||||||
|  | @ -54,14 +54,11 @@ const messages = defineMessages({ | ||||||
| 
 | 
 | ||||||
| const makeMapStateToProps = () => { | const makeMapStateToProps = () => { | ||||||
|   const getStatus = makeGetStatus(); |   const getStatus = makeGetStatus(); | ||||||
|  |   const getPictureInPicture = makeGetPictureInPicture(); | ||||||
| 
 | 
 | ||||||
|   const mapStateToProps = (state, props) => ({ |   const mapStateToProps = (state, props) => ({ | ||||||
|     status: getStatus(state, props), |     status: getStatus(state, props), | ||||||
| 
 |     pictureInPicture: getPictureInPicture(state, props), | ||||||
|     pictureInPicture: { |  | ||||||
|       inUse: state.getIn(['meta', 'layout']) !== 'mobile' && state.get('picture_in_picture').statusId === props.id, |  | ||||||
|       available: state.getIn(['meta', 'layout']) !== 'mobile', |  | ||||||
|     }, |  | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   return mapStateToProps; |   return mapStateToProps; | ||||||
|  |  | ||||||
|  | @ -20,9 +20,9 @@ import RadioButton from 'mastodon/components/radio_button'; | ||||||
| const messages = defineMessages({ | const messages = defineMessages({ | ||||||
|   deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' }, |   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' }, |   deleteConfirm: { id: 'confirmations.delete_list.confirm', defaultMessage: 'Delete' }, | ||||||
|   all_replies:   { id: 'lists.replies_policy.all_replies', defaultMessage: 'Any followed user' }, |   followed:   { id: 'lists.replies_policy.followed', defaultMessage: 'Any followed user' }, | ||||||
|   no_replies:    { id: 'lists.replies_policy.no_replies', defaultMessage: 'No one' }, |   none:    { id: 'lists.replies_policy.none', defaultMessage: 'No one' }, | ||||||
|   list_replies:  { id: 'lists.replies_policy.list_replies', defaultMessage: 'Members of the list' }, |   list:  { id: 'lists.replies_policy.list', defaultMessage: 'Members of the list' }, | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| const mapStateToProps = (state, props) => ({ | const mapStateToProps = (state, props) => ({ | ||||||
|  | @ -193,7 +193,7 @@ class ListTimeline extends React.PureComponent { | ||||||
|                 <FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' /> |                 <FormattedMessage id='lists.replies_policy.title' defaultMessage='Show replies to:' /> | ||||||
|               </span> |               </span> | ||||||
|               <div className='column-settings__row'> |               <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} /> |                   <RadioButton name='order' value={policy} label={intl.formatMessage(messages[policy])} checked={replies_policy === policy} onChange={this.handleRepliesPolicyChange} /> | ||||||
|                 ))} |                 ))} | ||||||
|               </div> |               </div> | ||||||
|  |  | ||||||
|  | @ -41,7 +41,10 @@ class DetailedStatus extends ImmutablePureComponent { | ||||||
|     domain: PropTypes.string.isRequired, |     domain: PropTypes.string.isRequired, | ||||||
|     compact: PropTypes.bool, |     compact: PropTypes.bool, | ||||||
|     showMedia: PropTypes.bool, |     showMedia: PropTypes.bool, | ||||||
|     usingPiP: PropTypes.bool, |     pictureInPicture: ImmutablePropTypes.contains({ | ||||||
|  |       inUse: PropTypes.bool, | ||||||
|  |       available: PropTypes.bool, | ||||||
|  |     }), | ||||||
|     onToggleMediaVisibility: PropTypes.func, |     onToggleMediaVisibility: PropTypes.func, | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  | @ -58,8 +61,8 @@ class DetailedStatus extends ImmutablePureComponent { | ||||||
|     e.stopPropagation(); |     e.stopPropagation(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   handleOpenVideo = (media, options) => { |   handleOpenVideo = (options) => { | ||||||
|     this.props.onOpenVideo(media, options); |     this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), options); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   handleExpandedToggle = () => { |   handleExpandedToggle = () => { | ||||||
|  | @ -102,7 +105,7 @@ class DetailedStatus extends ImmutablePureComponent { | ||||||
|   render () { |   render () { | ||||||
|     const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status; |     const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status; | ||||||
|     const outerStyle = { boxSizing: 'border-box' }; |     const outerStyle = { boxSizing: 'border-box' }; | ||||||
|     const { intl, compact, usingPiP } = this.props; |     const { intl, compact, pictureInPicture } = this.props; | ||||||
| 
 | 
 | ||||||
|     if (!status) { |     if (!status) { | ||||||
|       return null; |       return null; | ||||||
|  | @ -118,7 +121,7 @@ class DetailedStatus extends ImmutablePureComponent { | ||||||
|       outerStyle.height = `${this.state.height}px`; |       outerStyle.height = `${this.state.height}px`; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (usingPiP) { |     if (pictureInPicture.get('inUse')) { | ||||||
|       media = <PictureInPicturePlaceholder />; |       media = <PictureInPicturePlaceholder />; | ||||||
|     } else if (status.get('media_attachments').size > 0) { |     } else if (status.get('media_attachments').size > 0) { | ||||||
|       if (status.getIn(['media_attachments', 0, 'type']) === 'audio') { |       if (status.getIn(['media_attachments', 0, 'type']) === 'audio') { | ||||||
|  |  | ||||||
|  | @ -1,6 +1,6 @@ | ||||||
| import { connect } from 'react-redux'; | import { connect } from 'react-redux'; | ||||||
| import DetailedStatus from '../components/detailed_status'; | import DetailedStatus from '../components/detailed_status'; | ||||||
| import { makeGetStatus } from '../../../selectors'; | import { makeGetStatus, makeGetPictureInPicture } from '../../../selectors'; | ||||||
| import { | import { | ||||||
|   replyCompose, |   replyCompose, | ||||||
|   mentionCompose, |   mentionCompose, | ||||||
|  | @ -40,10 +40,12 @@ const messages = defineMessages({ | ||||||
| 
 | 
 | ||||||
| const makeMapStateToProps = () => { | const makeMapStateToProps = () => { | ||||||
|   const getStatus = makeGetStatus(); |   const getStatus = makeGetStatus(); | ||||||
|  |   const getPictureInPicture = makeGetPictureInPicture(); | ||||||
| 
 | 
 | ||||||
|   const mapStateToProps = (state, props) => ({ |   const mapStateToProps = (state, props) => ({ | ||||||
|     status: getStatus(state, props), |     status: getStatus(state, props), | ||||||
|     domain: state.getIn(['meta', 'domain']), |     domain: state.getIn(['meta', 'domain']), | ||||||
|  |     pictureInPicture: getPictureInPicture(state, props), | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   return mapStateToProps; |   return mapStateToProps; | ||||||
|  |  | ||||||
|  | @ -43,7 +43,7 @@ import { | ||||||
| import { initMuteModal } from '../../actions/mutes'; | import { initMuteModal } from '../../actions/mutes'; | ||||||
| import { initBlockModal } from '../../actions/blocks'; | import { initBlockModal } from '../../actions/blocks'; | ||||||
| import { initReport } from '../../actions/reports'; | import { initReport } from '../../actions/reports'; | ||||||
| import { makeGetStatus } from '../../selectors'; | import { makeGetStatus, makeGetPictureInPicture } from '../../selectors'; | ||||||
| import { ScrollContainer } from 'react-router-scroll-4'; | import { ScrollContainer } from 'react-router-scroll-4'; | ||||||
| import ColumnBackButton from '../../components/column_back_button'; | import ColumnBackButton from '../../components/column_back_button'; | ||||||
| import ColumnHeader from '../../components/column_header'; | import ColumnHeader from '../../components/column_header'; | ||||||
|  | @ -72,6 +72,7 @@ const messages = defineMessages({ | ||||||
| 
 | 
 | ||||||
| const makeMapStateToProps = () => { | const makeMapStateToProps = () => { | ||||||
|   const getStatus = makeGetStatus(); |   const getStatus = makeGetStatus(); | ||||||
|  |   const getPictureInPicture = makeGetPictureInPicture(); | ||||||
| 
 | 
 | ||||||
|   const getAncestorsIds = createSelector([ |   const getAncestorsIds = createSelector([ | ||||||
|     (_, { id }) => id, |     (_, { id }) => id, | ||||||
|  | @ -129,11 +130,12 @@ const makeMapStateToProps = () => { | ||||||
| 
 | 
 | ||||||
|   const mapStateToProps = (state, props) => { |   const mapStateToProps = (state, props) => { | ||||||
|     const status = getStatus(state, { id: props.params.statusId }); |     const status = getStatus(state, { id: props.params.statusId }); | ||||||
|     let ancestorsIds = Immutable.List(); | 
 | ||||||
|  |     let ancestorsIds   = Immutable.List(); | ||||||
|     let descendantsIds = Immutable.List(); |     let descendantsIds = Immutable.List(); | ||||||
| 
 | 
 | ||||||
|     if (status) { |     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') }); |       descendantsIds = getDescendantsIds(state, { id: status.get('id') }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -143,7 +145,7 @@ const makeMapStateToProps = () => { | ||||||
|       descendantsIds, |       descendantsIds, | ||||||
|       askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0, |       askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0, | ||||||
|       domain: state.getIn(['meta', 'domain']), |       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, |     askReplyConfirmation: PropTypes.bool, | ||||||
|     multiColumn: PropTypes.bool, |     multiColumn: PropTypes.bool, | ||||||
|     domain: PropTypes.string.isRequired, |     domain: PropTypes.string.isRequired, | ||||||
|     usingPiP: PropTypes.bool, |     pictureInPicture: ImmutablePropTypes.contains({ | ||||||
|  |       inUse: PropTypes.bool, | ||||||
|  |       available: PropTypes.bool, | ||||||
|  |     }), | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   state = { |   state = { | ||||||
|  | @ -492,7 +497,7 @@ class Status extends ImmutablePureComponent { | ||||||
| 
 | 
 | ||||||
|   render () { |   render () { | ||||||
|     let ancestors, descendants; |     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; |     const { fullscreen } = this.state; | ||||||
| 
 | 
 | ||||||
|     if (status === null) { |     if (status === null) { | ||||||
|  | @ -550,7 +555,7 @@ class Status extends ImmutablePureComponent { | ||||||
|                   domain={domain} |                   domain={domain} | ||||||
|                   showMedia={this.state.showMedia} |                   showMedia={this.state.showMedia} | ||||||
|                   onToggleMediaVisibility={this.handleToggleMediaVisibility} |                   onToggleMediaVisibility={this.handleToggleMediaVisibility} | ||||||
|                   usingPiP={usingPiP} |                   pictureInPicture={pictureInPicture} | ||||||
|                 /> |                 /> | ||||||
| 
 | 
 | ||||||
|                 <ActionBar |                 <ActionBar | ||||||
|  |  | ||||||
|  | @ -4,13 +4,11 @@ import PropTypes from 'prop-types'; | ||||||
| import Audio from 'mastodon/features/audio'; | import Audio from 'mastodon/features/audio'; | ||||||
| import { connect } from 'react-redux'; | import { connect } from 'react-redux'; | ||||||
| import ImmutablePureComponent from 'react-immutable-pure-component'; | import ImmutablePureComponent from 'react-immutable-pure-component'; | ||||||
| import { FormattedMessage } from 'react-intl'; |  | ||||||
| import { previewState } from './video_modal'; | import { previewState } from './video_modal'; | ||||||
| import classNames from 'classnames'; | import Footer from 'mastodon/features/picture_in_picture/components/footer'; | ||||||
| import Icon from 'mastodon/components/icon'; |  | ||||||
| 
 | 
 | ||||||
| const mapStateToProps = (state, { status }) => ({ | const mapStateToProps = (state, { statusId }) => ({ | ||||||
|   account: state.getIn(['accounts', status.get('account')]), |   accountStaticAvatar: state.getIn(['accounts', state.getIn(['statuses', statusId, 'account']), 'avatar_static']), | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| export default @connect(mapStateToProps) | export default @connect(mapStateToProps) | ||||||
|  | @ -18,12 +16,13 @@ class AudioModal extends ImmutablePureComponent { | ||||||
| 
 | 
 | ||||||
|   static propTypes = { |   static propTypes = { | ||||||
|     media: ImmutablePropTypes.map.isRequired, |     media: ImmutablePropTypes.map.isRequired, | ||||||
|     status: ImmutablePropTypes.map, |     statusId: PropTypes.string.isRequired, | ||||||
|  |     accountStaticAvatar: PropTypes.string.isRequired, | ||||||
|     options: PropTypes.shape({ |     options: PropTypes.shape({ | ||||||
|       autoPlay: PropTypes.bool, |       autoPlay: PropTypes.bool, | ||||||
|     }), |     }), | ||||||
|     account: ImmutablePropTypes.map, |  | ||||||
|     onClose: PropTypes.func.isRequired, |     onClose: PropTypes.func.isRequired, | ||||||
|  |     onChangeBackgroundColor: PropTypes.func.isRequired, | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   static contextTypes = { |   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 () { |   render () { | ||||||
|     const { media, status, account } = this.props; |     const { media, accountStaticAvatar, statusId, onClose } = this.props; | ||||||
|     const options = this.props.options || {}; |     const options = this.props.options || {}; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|  | @ -71,7 +63,7 @@ class AudioModal extends ImmutablePureComponent { | ||||||
|             alt={media.get('description')} |             alt={media.get('description')} | ||||||
|             duration={media.getIn(['meta', 'original', 'duration'], 0)} |             duration={media.getIn(['meta', 'original', 'duration'], 0)} | ||||||
|             height={150} |             height={150} | ||||||
|             poster={media.get('preview_url') || account.get('avatar_static')} |             poster={media.get('preview_url') || accountStaticAvatar} | ||||||
|             backgroundColor={media.getIn(['meta', 'colors', 'background'])} |             backgroundColor={media.getIn(['meta', 'colors', 'background'])} | ||||||
|             foregroundColor={media.getIn(['meta', 'colors', 'foreground'])} |             foregroundColor={media.getIn(['meta', 'colors', 'foreground'])} | ||||||
|             accentColor={media.getIn(['meta', 'colors', 'accent'])} |             accentColor={media.getIn(['meta', 'colors', 'accent'])} | ||||||
|  | @ -79,11 +71,9 @@ class AudioModal extends ImmutablePureComponent { | ||||||
|           /> |           /> | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         {status && ( |         <div className='media-modal__overlay'> | ||||||
|           <div className={classNames('media-modal__meta')}> |           {statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />} | ||||||
|             <a href={status.get('url')} onClick={this.handleStatusClick}><Icon id='comments' /> <FormattedMessage id='lightbox.view_context' defaultMessage='View context' /></a> |         </div> | ||||||
|           </div> |  | ||||||
|         )} |  | ||||||
|       </div> |       </div> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -75,7 +75,9 @@ class ColumnsArea extends ImmutablePureComponent { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   componentWillReceiveProps() { |   componentWillReceiveProps() { | ||||||
|     this.setState({ shouldAnimate: false }); |     if (typeof this.pendingIndex !== 'number' && this.lastIndex !== getIndex(this.context.router.history.location.pathname)) { | ||||||
|  |       this.setState({ shouldAnimate: false }); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   componentDidMount() { |   componentDidMount() { | ||||||
|  | @ -99,8 +101,13 @@ class ColumnsArea extends ImmutablePureComponent { | ||||||
|     if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) { |     if (this.props.singleColumn !== prevProps.singleColumn && !this.props.singleColumn) { | ||||||
|       this.node.addEventListener('wheel', this.handleWheel, supportsPassiveEvents ? { passive: true } : false); |       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 () { |   componentWillUnmount () { | ||||||
|  |  | ||||||
|  | @ -12,6 +12,7 @@ import Icon from 'mastodon/components/icon'; | ||||||
| import GIFV from 'mastodon/components/gifv'; | import GIFV from 'mastodon/components/gifv'; | ||||||
| import { disableSwiping } from 'mastodon/initial_state'; | import { disableSwiping } from 'mastodon/initial_state'; | ||||||
| import Footer from 'mastodon/features/picture_in_picture/components/footer'; | import Footer from 'mastodon/features/picture_in_picture/components/footer'; | ||||||
|  | import { getAverageFromBlurhash } from 'mastodon/blurhash'; | ||||||
| 
 | 
 | ||||||
| const messages = defineMessages({ | const messages = defineMessages({ | ||||||
|   close: { id: 'lightbox.close', defaultMessage: 'Close' }, |   close: { id: 'lightbox.close', defaultMessage: 'Close' }, | ||||||
|  | @ -21,111 +22,6 @@ const messages = defineMessages({ | ||||||
| 
 | 
 | ||||||
| export const previewState = 'previewMediaModal'; | 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 | export default @injectIntl | ||||||
| class MediaModal extends ImmutablePureComponent { | class MediaModal extends ImmutablePureComponent { | ||||||
| 
 | 
 | ||||||
|  | @ -224,7 +120,7 @@ class MediaModal extends ImmutablePureComponent { | ||||||
|     const blurhash = media.getIn([index, 'blurhash']); |     const blurhash = media.getIn([index, 'blurhash']); | ||||||
| 
 | 
 | ||||||
|     if (blurhash) { |     if (blurhash) { | ||||||
|       const backgroundColor = decodeRGB(decode83(blurhash.slice(2, 6))); |       const backgroundColor = getAverageFromBlurhash(blurhash); | ||||||
|       onChangeBackgroundColor(backgroundColor); |       onChangeBackgroundColor(backgroundColor); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -3,6 +3,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||||
| import PropTypes from 'prop-types'; | import PropTypes from 'prop-types'; | ||||||
| import Video from 'mastodon/features/video'; | import Video from 'mastodon/features/video'; | ||||||
| import ImmutablePureComponent from 'react-immutable-pure-component'; | 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'; | export const previewState = 'previewVideoModal'; | ||||||
| 
 | 
 | ||||||
|  | @ -17,6 +19,7 @@ export default class VideoModal extends ImmutablePureComponent { | ||||||
|       defaultVolume: PropTypes.number, |       defaultVolume: PropTypes.number, | ||||||
|     }), |     }), | ||||||
|     onClose: PropTypes.func.isRequired, |     onClose: PropTypes.func.isRequired, | ||||||
|  |     onChangeBackgroundColor: PropTypes.func.isRequired, | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   static contextTypes = { |   static contextTypes = { | ||||||
|  | @ -24,29 +27,35 @@ export default class VideoModal extends ImmutablePureComponent { | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   componentDidMount () { |   componentDidMount () { | ||||||
|     if (this.context.router) { |     const { router } = this.context; | ||||||
|       const history = this.context.router.history; |     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(() => { |     const backgroundColor = getAverageFromBlurhash(media.get('blurhash')); | ||||||
|         this.props.onClose(); | 
 | ||||||
|       }); |     if (backgroundColor) { | ||||||
|  |       onChangeBackgroundColor(backgroundColor); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   componentWillUnmount () { |   componentWillUnmount () { | ||||||
|     if (this.context.router) { |     const { router } = this.context; | ||||||
|  | 
 | ||||||
|  |     if (router) { | ||||||
|       this.unlistenHistory(); |       this.unlistenHistory(); | ||||||
| 
 | 
 | ||||||
|       if (this.context.router.history.location.state === previewState) { |       if (router.history.location.state === previewState) { | ||||||
|         this.context.router.history.goBack(); |         router.history.goBack(); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   render () { |   render () { | ||||||
|     const { media, onClose } = this.props; |     const { media, statusId, onClose } = this.props; | ||||||
|     const options = this.props.options || {}; |     const options = this.props.options || {}; | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|  | @ -65,6 +74,10 @@ export default class VideoModal extends ImmutablePureComponent { | ||||||
|             alt={media.get('description')} |             alt={media.get('description')} | ||||||
|           /> |           /> | ||||||
|         </div> |         </div> | ||||||
|  | 
 | ||||||
|  |         <div className='media-modal__overlay'> | ||||||
|  |           {statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />} | ||||||
|  |         </div> | ||||||
|       </div> |       </div> | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -1,7 +1,7 @@ | ||||||
| import React from 'react'; | import React from 'react'; | ||||||
| import PropTypes from 'prop-types'; | import PropTypes from 'prop-types'; | ||||||
| import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; | import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; | ||||||
| import { fromJS, is } from 'immutable'; | import { is } from 'immutable'; | ||||||
| import { throttle, debounce } from 'lodash'; | import { throttle, debounce } from 'lodash'; | ||||||
| import classNames from 'classnames'; | import classNames from 'classnames'; | ||||||
| import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen'; | import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen'; | ||||||
|  | @ -495,25 +495,13 @@ class Video extends React.PureComponent { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   handleOpenVideo = () => { |   handleOpenVideo = () => { | ||||||
|     const { src, preview, width, height, alt } = this.props; |     this.video.pause(); | ||||||
| 
 | 
 | ||||||
|     const media = fromJS({ |     this.props.onOpenVideo({ | ||||||
|       type: 'video', |  | ||||||
|       url: src, |  | ||||||
|       preview_url: preview, |  | ||||||
|       description: alt, |  | ||||||
|       width, |  | ||||||
|       height, |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     const options = { |  | ||||||
|       startTime: this.video.currentTime, |       startTime: this.video.currentTime, | ||||||
|       autoPlay: !this.state.paused, |       autoPlay: !this.state.paused, | ||||||
|       defaultVolume: this.state.volume, |       defaultVolume: this.state.volume, | ||||||
|     }; |     }); | ||||||
| 
 |  | ||||||
|     this.video.pause(); |  | ||||||
|     this.props.onOpenVideo(media, options); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   handleCloseVideo = () => { |   handleCloseVideo = () => { | ||||||
|  |  | ||||||
|  | @ -1,5 +1,5 @@ | ||||||
| import { createSelector } from 'reselect'; | 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'; | import { me } from '../initial_state'; | ||||||
| 
 | 
 | ||||||
| const getAccountBase         = (state, id) => state.getIn(['accounts', id], null); | 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'); | const getAlertsBase = state => state.get('alerts'); | ||||||
| 
 | 
 | ||||||
| export const getAlerts = createSelector([getAlertsBase], (base) => { | export const getAlerts = createSelector([getAlertsBase], (base) => { | ||||||
|  |  | ||||||
|  | @ -403,8 +403,8 @@ class FeedManager | ||||||
|   def filter_from_list?(status, list) |   def filter_from_list?(status, list) | ||||||
|     if status.reply? && status.in_reply_to_account_id != status.account_id |     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 = status.in_reply_to_account_id != list.account_id | ||||||
|       should_filter &&= !list.show_all_replies? |       should_filter &&= !list.show_followed? | ||||||
|       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_list? && ListAccount.where(list_id: list.id, account_id: status.in_reply_to_account_id).exists?) | ||||||
| 
 | 
 | ||||||
|       return !!should_filter |       return !!should_filter | ||||||
|     end |     end | ||||||
|  |  | ||||||
|  | @ -445,7 +445,7 @@ class Account < ApplicationRecord | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|     def inboxes |     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) |       DeliveryFailureTracker.without_unavailable(urls) | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -51,7 +51,7 @@ class Form::AccountBatch | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def account_domains |   def account_domains | ||||||
|     accounts.pluck(Arel.sql('distinct domain')).compact |     accounts.group(:domain).pluck(:domain).compact | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def accounts |   def accounts | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ | ||||||
| #  title          :string           default(""), not null | #  title          :string           default(""), not null | ||||||
| #  created_at     :datetime         not null | #  created_at     :datetime         not null | ||||||
| #  updated_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 | class List < ApplicationRecord | ||||||
|  | @ -16,7 +16,7 @@ class List < ApplicationRecord | ||||||
| 
 | 
 | ||||||
|   PER_ACCOUNT_LIMIT = 50 |   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 |   belongs_to :account, optional: true | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -9,10 +9,6 @@ class REST::AccountFeaturedTagSerializer < ActiveModel::Serializer | ||||||
|     object.tag.id.to_s |     object.tag.id.to_s | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def name |  | ||||||
|     "##{object.name}" |  | ||||||
|   end |  | ||||||
| 
 |  | ||||||
|   def url |   def url | ||||||
|     short_account_tag_url(object.account, object.tag) |     short_account_tag_url(object.account, object.tag) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  | @ -342,7 +342,7 @@ RSpec.describe FeedManager do | ||||||
| 
 | 
 | ||||||
|     context 'when replies policy is set to no replies' do |     context 'when replies policy is set to no replies' do | ||||||
|       before do |       before do | ||||||
|         list.replies_policy = :no_replies |         list.replies_policy = :none | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it 'pushes statuses that are not replies' do |       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 |     context 'when replies policy is set to list-only replies' do | ||||||
|       before do |       before do | ||||||
|         list.replies_policy = :list_replies |         list.replies_policy = :list | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it 'pushes statuses that are not replies' do |       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 |     context 'when replies policy is set to any reply' do | ||||||
|       before do |       before do | ||||||
|         list.replies_policy = :all_replies |         list.replies_policy = :followed | ||||||
|       end |       end | ||||||
| 
 | 
 | ||||||
|       it 'pushes statuses that are not replies' do |       it 'pushes statuses that are not replies' do | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue