Implement a click-to-view spoiler system
This commit is contained in:
		
							parent
							
								
									1761d3f9c3
								
							
						
					
					
						commit
						bf0f6eb62d
					
				
					 18 changed files with 192 additions and 77 deletions
				
			
		|  | @ -23,6 +23,8 @@ export const COMPOSE_MOUNT   = 'COMPOSE_MOUNT'; | |||
| export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT'; | ||||
| 
 | ||||
| export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE'; | ||||
| export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE'; | ||||
| export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE'; | ||||
| export const COMPOSE_VISIBILITY_CHANGE  = 'COMPOSE_VISIBILITY_CHANGE'; | ||||
| export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE'; | ||||
| 
 | ||||
|  | @ -68,6 +70,8 @@ export function submitCompose() { | |||
|       in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), | ||||
|       media_ids: getState().getIn(['compose', 'media_attachments']).map(item => item.get('id')), | ||||
|       sensitive: getState().getIn(['compose', 'sensitive']), | ||||
|       spoiler: getState().getIn(['compose', 'spoiler']), | ||||
|       spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''), | ||||
|       visibility: getState().getIn(['compose', 'private']) ? 'private' : (getState().getIn(['compose', 'unlisted']) ? 'unlisted' : 'public') | ||||
|     }).then(function (response) { | ||||
|       dispatch(submitComposeSuccess({ ...response.data })); | ||||
|  | @ -218,6 +222,20 @@ export function changeComposeSensitivity(checked) { | |||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function changeComposeSpoilerness(checked) { | ||||
|   return { | ||||
|     type: COMPOSE_SPOILERNESS_CHANGE, | ||||
|     checked | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function changeComposeSpoilerText(text) { | ||||
|   return { | ||||
|     type: COMPOSE_SPOILER_TEXT_CHANGE, | ||||
|     text | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| export function changeComposeVisibility(checked) { | ||||
|   return { | ||||
|     type: COMPOSE_VISIBILITY_CHANGE, | ||||
|  |  | |||
|  | @ -18,6 +18,12 @@ const StatusContent = React.createClass({ | |||
|   componentDidMount () { | ||||
|     const node  = ReactDOM.findDOMNode(this); | ||||
|     const links = node.querySelectorAll('a'); | ||||
|     const spoilers = node.querySelectorAll('.spoiler'); | ||||
| 
 | ||||
|     for (var i = 0; i < spoilers.length; ++i) { | ||||
|       let spoiler    = spoilers[i]; | ||||
|       spoiler.addEventListener('click', this.onSpoilerClick.bind(this, spoiler), true); | ||||
|     } | ||||
| 
 | ||||
|     for (var i = 0; i < links.length; ++i) { | ||||
|       let link    = links[i]; | ||||
|  | @ -52,6 +58,18 @@ const StatusContent = React.createClass({ | |||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   onSpoilerClick (spoiler, e) { | ||||
|     if (e.button === 0) { | ||||
|       //only toggle if we're not clicking a visible link | ||||
|       var hasClass = $(spoiler).hasClass('spoiler-on'); | ||||
|       if (hasClass || e.target === spoiler) { | ||||
|         e.stopPropagation(); | ||||
|         e.preventDefault(); | ||||
|         $(spoiler).siblings(".spoiler").andSelf().toggleClass('spoiler-on', !hasClass); | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   onNormalClick (e) { | ||||
|     e.stopPropagation(); | ||||
|   }, | ||||
|  |  | |||
|  | @ -14,6 +14,7 @@ import { Motion, spring } from 'react-motion'; | |||
| 
 | ||||
| const messages = defineMessages({ | ||||
|   placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' }, | ||||
|   spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Content warning' }, | ||||
|   publish: { id: 'compose_form.publish', defaultMessage: 'Publish' } | ||||
| }); | ||||
| 
 | ||||
|  | @ -25,6 +26,8 @@ const ComposeForm = React.createClass({ | |||
|     suggestion_token: React.PropTypes.string, | ||||
|     suggestions: ImmutablePropTypes.list, | ||||
|     sensitive: React.PropTypes.bool, | ||||
|     spoiler: React.PropTypes.bool, | ||||
|     spoiler_text: React.PropTypes.string, | ||||
|     unlisted: React.PropTypes.bool, | ||||
|     private: React.PropTypes.bool, | ||||
|     fileDropDate: React.PropTypes.instanceOf(Date), | ||||
|  | @ -40,6 +43,8 @@ const ComposeForm = React.createClass({ | |||
|     onFetchSuggestions: React.PropTypes.func.isRequired, | ||||
|     onSuggestionSelected: React.PropTypes.func.isRequired, | ||||
|     onChangeSensitivity: React.PropTypes.func.isRequired, | ||||
|     onChangeSpoilerness: React.PropTypes.func.isRequired, | ||||
|     onChangeSpoilerText: React.PropTypes.func.isRequired, | ||||
|     onChangeVisibility: React.PropTypes.func.isRequired, | ||||
|     onChangeListability: React.PropTypes.func.isRequired, | ||||
|   }, | ||||
|  | @ -77,6 +82,15 @@ const ComposeForm = React.createClass({ | |||
|     this.props.onChangeSensitivity(e.target.checked); | ||||
|   }, | ||||
| 
 | ||||
|   handleChangeSpoilerness (e) { | ||||
|     this.props.onChangeSpoilerness(e.target.checked); | ||||
|     this.props.onChangeSpoilerText(''); | ||||
|   }, | ||||
| 
 | ||||
|   handleChangeSpoilerText (e) { | ||||
|     this.props.onChangeSpoilerText(e.target.value); | ||||
|   }, | ||||
| 
 | ||||
|   handleChangeVisibility (e) { | ||||
|     this.props.onChangeVisibility(e.target.checked); | ||||
|   }, | ||||
|  | @ -115,6 +129,14 @@ const ComposeForm = React.createClass({ | |||
| 
 | ||||
|     return ( | ||||
|       <div style={{ padding: '10px' }}> | ||||
|         <Motion defaultStyle={{ opacity: !this.props.spoiler ? 0 : 100, height: !this.props.spoiler ? 50 : 0 }} style={{ opacity: spring(!this.props.spoiler ? 0 : 100), height: spring(!this.props.spoiler ? 0 : 50) }}> | ||||
|           {({ opacity, height }) => | ||||
|             <div className="spoiler-input" style={{ height: `${height}px`, overflow: 'hidden', opacity: opacity / 100 }}> | ||||
|               <input placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={this.props.spoiler_text} onChange={this.handleChangeSpoilerText} type="text" className="spoiler-input__input" /> | ||||
|             </div> | ||||
|           } | ||||
|         </Motion> | ||||
| 
 | ||||
|         {replyArea} | ||||
| 
 | ||||
|         <AutosuggestTextarea | ||||
|  | @ -133,7 +155,7 @@ const ComposeForm = React.createClass({ | |||
| 
 | ||||
|         <div style={{ marginTop: '10px', overflow: 'hidden' }}> | ||||
|           <div style={{ float: 'right' }}><Button text={intl.formatMessage(messages.publish)} onClick={this.handleSubmit} disabled={disabled} /></div> | ||||
|           <div style={{ float: 'right', marginRight: '16px', lineHeight: '36px' }}><CharacterCounter max={500} text={this.props.text} /></div> | ||||
|           <div style={{ float: 'right', marginRight: '16px', lineHeight: '36px' }}><CharacterCounter max={500} text={this.props.spoiler ? (this.props.spoiler_text + "\n" + this.props.text) : this.props.text} /></div> | ||||
|           <UploadButtonContainer style={{ paddingTop: '4px' }} /> | ||||
|         </div> | ||||
| 
 | ||||
|  | @ -142,6 +164,11 @@ const ComposeForm = React.createClass({ | |||
|           <span style={{ display: 'inline-block', verticalAlign: 'middle', marginBottom: '14px', marginLeft: '8px', color: '#9baec8' }}><FormattedMessage id='compose_form.private' defaultMessage='Mark as private' /></span> | ||||
|         </label> | ||||
| 
 | ||||
|         <label style={{ display: 'block', lineHeight: '24px', verticalAlign: 'middle' }}> | ||||
|           <Toggle checked={this.props.spoiler} onChange={this.handleChangeSpoilerness} /> | ||||
|           <span style={{ display: 'inline-block', verticalAlign: 'middle', marginBottom: '14px', marginLeft: '8px', color: '#9baec8' }}><FormattedMessage id='compose_form.spoiler' defaultMessage='Hide behind content warning' /></span> | ||||
|         </label> | ||||
| 
 | ||||
|         <Motion defaultStyle={{ opacity: (this.props.private || reply_to_other) ? 0 : 100, height: (this.props.private || reply_to_other) ? 39.5 : 0 }} style={{ opacity: spring((this.props.private || reply_to_other) ? 0 : 100), height: spring((this.props.private || reply_to_other) ? 0 : 39.5) }}> | ||||
|           {({ opacity, height }) => | ||||
|             <label style={{ display: 'block', lineHeight: '24px', verticalAlign: 'middle', height: `${height}px`, overflow: 'hidden', opacity: opacity / 100 }}> | ||||
|  |  | |||
|  | @ -8,6 +8,8 @@ import { | |||
|   fetchComposeSuggestions, | ||||
|   selectComposeSuggestion, | ||||
|   changeComposeSensitivity, | ||||
|   changeComposeSpoilerness, | ||||
|   changeComposeSpoilerText, | ||||
|   changeComposeVisibility, | ||||
|   changeComposeListability | ||||
| } from '../../../actions/compose'; | ||||
|  | @ -22,6 +24,8 @@ const makeMapStateToProps = () => { | |||
|       suggestion_token: state.getIn(['compose', 'suggestion_token']), | ||||
|       suggestions: state.getIn(['compose', 'suggestions']), | ||||
|       sensitive: state.getIn(['compose', 'sensitive']), | ||||
|       spoiler: state.getIn(['compose', 'spoiler']), | ||||
|       spoiler_text: state.getIn(['compose', 'spoiler_text']), | ||||
|       unlisted: state.getIn(['compose', 'unlisted']), | ||||
|       private: state.getIn(['compose', 'private']), | ||||
|       fileDropDate: state.getIn(['compose', 'fileDropDate']), | ||||
|  | @ -66,6 +70,14 @@ const mapDispatchToProps = function (dispatch) { | |||
|       dispatch(changeComposeSensitivity(checked)); | ||||
|     }, | ||||
| 
 | ||||
|     onChangeSpoilerness (checked) { | ||||
|       dispatch(changeComposeSpoilerness(checked)); | ||||
|     }, | ||||
| 
 | ||||
|     onChangeSpoilerText (checked) { | ||||
|       dispatch(changeComposeSpoilerText(checked)); | ||||
|     }, | ||||
| 
 | ||||
|     onChangeVisibility (checked) { | ||||
|       dispatch(changeComposeVisibility(checked)); | ||||
|     }, | ||||
|  |  | |||
|  | @ -17,6 +17,8 @@ import { | |||
|   COMPOSE_SUGGESTIONS_READY, | ||||
|   COMPOSE_SUGGESTION_SELECT, | ||||
|   COMPOSE_SENSITIVITY_CHANGE, | ||||
|   COMPOSE_SPOILERNESS_CHANGE, | ||||
|   COMPOSE_SPOILER_TEXT_CHANGE, | ||||
|   COMPOSE_VISIBILITY_CHANGE, | ||||
|   COMPOSE_LISTABILITY_CHANGE | ||||
| } from '../actions/compose'; | ||||
|  | @ -27,6 +29,8 @@ import Immutable from 'immutable'; | |||
| const initialState = Immutable.Map({ | ||||
|   mounted: false, | ||||
|   sensitive: false, | ||||
|   spoiler: false, | ||||
|   spoiler_text: '', | ||||
|   unlisted: false, | ||||
|   private: false, | ||||
|   text: '', | ||||
|  | @ -56,6 +60,8 @@ function statusToTextMentions(state, status) { | |||
| function clearAll(state) { | ||||
|   return state.withMutations(map => { | ||||
|     map.set('text', ''); | ||||
|     map.set('spoiler', false); | ||||
|     map.set('spoiler_text', ''); | ||||
|     map.set('is_submitting', false); | ||||
|     map.set('in_reply_to', null); | ||||
|     map.update('media_attachments', list => list.clear()); | ||||
|  | @ -98,6 +104,10 @@ export default function compose(state = initialState, action) { | |||
|       return state.set('mounted', false); | ||||
|     case COMPOSE_SENSITIVITY_CHANGE: | ||||
|       return state.set('sensitive', action.checked); | ||||
|     case COMPOSE_SPOILERNESS_CHANGE: | ||||
|       return state.set('spoiler', action.checked); | ||||
|     case COMPOSE_SPOILER_TEXT_CHANGE: | ||||
|       return state.set('spoiler_text', action.text); | ||||
|     case COMPOSE_VISIBILITY_CHANGE: | ||||
|       return state.set('private', action.checked); | ||||
|     case COMPOSE_LISTABILITY_CHANGE: | ||||
|  |  | |||
|  | @ -14,6 +14,16 @@ $(() => { | |||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   $.each($('.spoiler'), (_, content) => { | ||||
|     $(content).on('click', e => { | ||||
|       var hasClass = $(content).hasClass('spoiler-on'); | ||||
|       if (hasClass || e.target === content) { | ||||
|         e.preventDefault(); | ||||
|         $(content).siblings(".spoiler").andSelf().toggleClass('spoiler-on', !hasClass); | ||||
|       } | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   $('.media-spoiler').on('click', e => { | ||||
|     $(e.target).hide(); | ||||
|   }); | ||||
|  |  | |||
|  | @ -584,21 +584,20 @@ | |||
|   } | ||||
| } | ||||
| 
 | ||||
| .autosuggest-textarea { | ||||
| .autosuggest-textarea, .spoiler-input { | ||||
|   position: relative; | ||||
| } | ||||
| 
 | ||||
| .autosuggest-textarea__textarea { | ||||
| .autosuggest-textarea__textarea, .spoiler-input__input { | ||||
|   display: block; | ||||
|   box-sizing: border-box; | ||||
|   width: 100%; | ||||
|   height: 100px; | ||||
|   resize: none; | ||||
|   margin: 0; | ||||
|   color: $color1; | ||||
|   padding: 7px; | ||||
|   font-family: inherit; | ||||
|   font-size: 14px; | ||||
|   margin: 0; | ||||
|   resize: vertical; | ||||
| 
 | ||||
|   border: 3px dashed transparent; | ||||
|  | @ -609,6 +608,10 @@ | |||
|   } | ||||
| } | ||||
| 
 | ||||
| .autosuggest-textarea__textarea { | ||||
|   height: 100px; | ||||
| } | ||||
| 
 | ||||
| .autosuggest-textarea__suggestions { | ||||
|   position: absolute; | ||||
|   top: 100%; | ||||
|  | @ -663,6 +666,39 @@ | |||
|   } | ||||
| } | ||||
| 
 | ||||
| .spoiler-helper { | ||||
|   margin-bottom: -20px !important; | ||||
| } | ||||
| 
 | ||||
| .spoiler { | ||||
|   &::before { | ||||
|     margin-top: 20px; | ||||
|     display: block; | ||||
|     content: ''; | ||||
|   } | ||||
| 
 | ||||
|   display: inline; | ||||
|   cursor: pointer; | ||||
|   border-bottom: 1px dashed white; | ||||
|   .light & { | ||||
|     border-bottom: 1px dashed black; | ||||
|   } | ||||
| 
 | ||||
|   &.spoiler-on { | ||||
|     &, & * { | ||||
|       color: transparent !important; | ||||
|     } | ||||
|     background: white; | ||||
|     .light & { | ||||
|       background: black; | ||||
|     } | ||||
| 
 | ||||
|     .emojione { | ||||
|       opacity: 0; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| button i.fa-retweet { | ||||
|   height: 19px; | ||||
|   width: 22px; | ||||
|  |  | |||
|  | @ -57,7 +57,7 @@ class Api::V1::StatusesController < ApiController | |||
|   end | ||||
| 
 | ||||
|   def create | ||||
|     @status = PostStatusService.new.call(current_user.account, params[:status], params[:in_reply_to_id].blank? ? nil : Status.find(params[:in_reply_to_id]), media_ids: params[:media_ids], sensitive: params[:sensitive], visibility: params[:visibility], application: doorkeeper_token.application) | ||||
|     @status = PostStatusService.new.call(current_user.account, params[:status], params[:in_reply_to_id].blank? ? nil : Status.find(params[:in_reply_to_id]), media_ids: params[:media_ids], sensitive: params[:sensitive], spoiler: params[:spoiler], spoiler_text: params[:spoiler_text], visibility: params[:visibility], application: doorkeeper_token.application) | ||||
|     render action: :show | ||||
|   end | ||||
| 
 | ||||
|  |  | |||
|  | @ -207,6 +207,7 @@ module AtomBuilderHelper | |||
|           end | ||||
| 
 | ||||
|           category(xml, 'nsfw') if stream_entry.target.sensitive? | ||||
|           category(xml, 'spoiler') if stream_entry.target.spoiler? | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|  | @ -228,6 +229,7 @@ module AtomBuilderHelper | |||
|     end | ||||
| 
 | ||||
|     category(xml, 'nsfw') if stream_entry.activity.sensitive? | ||||
|     category(xml, 'spoiler') if stream_entry.activity.spoiler? | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
|  |  | |||
|  | @ -14,7 +14,15 @@ class Formatter | |||
| 
 | ||||
|     html = status.text | ||||
|     html = encode(html) | ||||
|     html = simple_format(html, sanitize: false) | ||||
| 
 | ||||
|     if (status.spoiler?) | ||||
|       spoilerhtml = status.spoiler_text | ||||
|       spoilerhtml = encode(spoilerhtml) | ||||
|       html = wrap_spoilers(html, spoilerhtml) | ||||
|     else | ||||
|       html = simple_format(html, sanitize: false) | ||||
|     end | ||||
| 
 | ||||
|     html = html.gsub(/\n/, '') | ||||
|     html = link_urls(html) | ||||
|     html = link_mentions(html, status.mentions) | ||||
|  | @ -43,6 +51,13 @@ class Formatter | |||
|     HTMLEntities.new.encode(html) | ||||
|   end | ||||
| 
 | ||||
|   def wrap_spoilers(html, spoilerhtml) | ||||
|     spoilerhtml = simple_format(spoilerhtml, {class: "spoiler-helper"}, {sanitize: false}) | ||||
|     html = simple_format(html, {class: ["spoiler", "spoiler-on"]}, {sanitize: false}) | ||||
| 
 | ||||
|     spoilerhtml + html | ||||
|   end | ||||
| 
 | ||||
|   def link_urls(html) | ||||
|     html.gsub(URI.regexp(%w(http https))) do |match| | ||||
|       link_html(match) | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class Status < ApplicationRecord | ||||
|   include ActiveModel::Validations | ||||
|   include Paginable | ||||
|   include Streamable | ||||
|   include Cacheable | ||||
|  | @ -27,7 +28,8 @@ class Status < ApplicationRecord | |||
| 
 | ||||
|   validates :account, presence: true | ||||
|   validates :uri, uniqueness: true, unless: 'local?' | ||||
|   validates :text, presence: true, length: { maximum: 500 }, if: proc { |s| s.local? && !s.reblog? } | ||||
|   validates :text, presence: true, if: proc { |s| s.local? && !s.reblog? } | ||||
|   validates_with StatusLengthValidator | ||||
|   validates :text, presence: true, if: proc { |s| !s.local? && !s.reblog? } | ||||
|   validates :reblog, uniqueness: { scope: :account, message: 'of status already exists' }, if: 'reblog?' | ||||
| 
 | ||||
|  |  | |||
|  | @ -8,6 +8,8 @@ class PostStatusService < BaseService | |||
|   # @param [Hash] options | ||||
|   # @option [Boolean] :sensitive | ||||
|   # @option [String] :visibility | ||||
|   # @option [Boolean] :spoiler | ||||
|   # @option [String] :spoiler_text | ||||
|   # @option [Enumerable] :media_ids Optional array of media IDs to attach | ||||
|   # @option [Doorkeeper::Application] :application | ||||
|   # @return [Status] | ||||
|  | @ -15,6 +17,8 @@ class PostStatusService < BaseService | |||
|     status = account.statuses.create!(text:        text, | ||||
|                                       thread:      in_reply_to, | ||||
|                                       sensitive:   options[:sensitive], | ||||
|                                       spoiler:   options[:spoiler], | ||||
|                                       spoiler_text:   options[:spoiler_text], | ||||
|                                       visibility:  options[:visibility], | ||||
|                                       application: options[:application]) | ||||
| 
 | ||||
|  |  | |||
|  | @ -9,5 +9,6 @@ class ProcessHashtagsService < BaseService | |||
|     end | ||||
| 
 | ||||
|     status.update(sensitive: true) if tags.include?('nsfw') | ||||
|     status.update(spoiler: true) if tags.include?('spoiler') | ||||
|   end | ||||
| end | ||||
|  |  | |||
							
								
								
									
										15
									
								
								app/validators/status_length_validator.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								app/validators/status_length_validator.rb
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| class StatusLengthValidator < ActiveModel::Validator | ||||
|   def validate(status) | ||||
|     if status.local? && !status.reblog? | ||||
|       combinedText = status.text | ||||
|       if (status.spoiler? && status.spoiler_text.present?) | ||||
|         combinedText = status.spoiler_text + "\n" + status.text | ||||
|       end | ||||
| 
 | ||||
|       maxChars = 500 | ||||
|       unless combinedText.length <= maxChars | ||||
|         status.errors[:text] << "is too long (maximum is #{maxChars})" | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -1,4 +1,4 @@ | |||
| attributes :id, :created_at, :in_reply_to_id, :sensitive, :visibility | ||||
| attributes :id, :created_at, :in_reply_to_id, :sensitive, :spoiler, :visibility | ||||
| 
 | ||||
| node(:uri)              { |status| TagManager.instance.uri_for(status) } | ||||
| node(:content)          { |status| Formatter.instance.format(status) } | ||||
|  |  | |||
							
								
								
									
										5
									
								
								db/migrate/20170112041538_add_spoiler_to_statuses.rb
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								db/migrate/20170112041538_add_spoiler_to_statuses.rb
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| class AddSpoilerToStatuses < ActiveRecord::Migration[5.0] | ||||
|   def change | ||||
|     add_column :statuses, :spoiler, :boolean, default: false | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,5 @@ | |||
| class AddSpoilerTextToStatuses < ActiveRecord::Migration[5.0] | ||||
|   def change | ||||
|     add_column :statuses, :spoiler_text, :text, default: "" | ||||
|   end | ||||
| end | ||||
							
								
								
									
										71
									
								
								db/schema.rb
									
										
									
									
									
								
							
							
						
						
									
										71
									
								
								db/schema.rb
									
										
									
									
									
								
							|  | @ -186,74 +186,6 @@ ActiveRecord::Schema.define(version: 20170123203248) do | |||
|     t.index ["topic", "callback"], name: "index_pubsubhubbub_subscriptions_on_topic_and_callback", unique: true, using: :btree | ||||
|   end | ||||
| 
 | ||||
|   create_table "push_devices", force: :cascade do |t| | ||||
|     t.string   "service",    default: "", null: false | ||||
|     t.string   "token",      default: "", null: false | ||||
|     t.integer  "account",                 null: false | ||||
|     t.datetime "created_at",              null: false | ||||
|     t.datetime "updated_at",              null: false | ||||
|     t.index ["service", "token"], name: "index_push_devices_on_service_and_token", unique: true, using: :btree | ||||
|   end | ||||
| 
 | ||||
|   create_table "rpush_apps", force: :cascade do |t| | ||||
|     t.string   "name",                                null: false | ||||
|     t.string   "environment" | ||||
|     t.text     "certificate" | ||||
|     t.string   "password" | ||||
|     t.integer  "connections",             default: 1, null: false | ||||
|     t.datetime "created_at",                          null: false | ||||
|     t.datetime "updated_at",                          null: false | ||||
|     t.string   "type",                                null: false | ||||
|     t.string   "auth_key" | ||||
|     t.string   "client_id" | ||||
|     t.string   "client_secret" | ||||
|     t.string   "access_token" | ||||
|     t.datetime "access_token_expiration" | ||||
|   end | ||||
| 
 | ||||
|   create_table "rpush_feedback", force: :cascade do |t| | ||||
|     t.string   "device_token", limit: 64, null: false | ||||
|     t.datetime "failed_at",               null: false | ||||
|     t.datetime "created_at",              null: false | ||||
|     t.datetime "updated_at",              null: false | ||||
|     t.integer  "app_id" | ||||
|     t.index ["device_token"], name: "index_rpush_feedback_on_device_token", using: :btree | ||||
|   end | ||||
| 
 | ||||
|   create_table "rpush_notifications", force: :cascade do |t| | ||||
|     t.integer  "badge" | ||||
|     t.string   "device_token",      limit: 64 | ||||
|     t.string   "sound",                        default: "default" | ||||
|     t.text     "alert" | ||||
|     t.text     "data" | ||||
|     t.integer  "expiry",                       default: 86400 | ||||
|     t.boolean  "delivered",                    default: false,     null: false | ||||
|     t.datetime "delivered_at" | ||||
|     t.boolean  "failed",                       default: false,     null: false | ||||
|     t.datetime "failed_at" | ||||
|     t.integer  "error_code" | ||||
|     t.text     "error_description" | ||||
|     t.datetime "deliver_after" | ||||
|     t.datetime "created_at",                                       null: false | ||||
|     t.datetime "updated_at",                                       null: false | ||||
|     t.boolean  "alert_is_json",                default: false | ||||
|     t.string   "type",                                             null: false | ||||
|     t.string   "collapse_key" | ||||
|     t.boolean  "delay_while_idle",             default: false,     null: false | ||||
|     t.text     "registration_ids" | ||||
|     t.integer  "app_id",                                           null: false | ||||
|     t.integer  "retries",                      default: 0 | ||||
|     t.string   "uri" | ||||
|     t.datetime "fail_after" | ||||
|     t.boolean  "processing",                   default: false,     null: false | ||||
|     t.integer  "priority" | ||||
|     t.text     "url_args" | ||||
|     t.string   "category" | ||||
|     t.boolean  "content_available",            default: false | ||||
|     t.text     "notification" | ||||
|     t.index ["delivered", "failed"], name: "index_rpush_notifications_multi", where: "((NOT delivered) AND (NOT failed))", using: :btree | ||||
|   end | ||||
| 
 | ||||
|   create_table "settings", force: :cascade do |t| | ||||
|     t.string   "var",        null: false | ||||
|     t.text     "value" | ||||
|  | @ -276,6 +208,9 @@ ActiveRecord::Schema.define(version: 20170123203248) do | |||
|     t.boolean  "sensitive",              default: false | ||||
|     t.integer  "visibility",             default: 0,     null: false | ||||
|     t.integer  "in_reply_to_account_id" | ||||
|     t.string   "conversation_uri" | ||||
|     t.boolean  "spoiler",                default: false | ||||
|     t.text     "spoiler_text",           default: "" | ||||
|     t.integer  "application_id" | ||||
|     t.index ["account_id"], name: "index_statuses_on_account_id", using: :btree | ||||
|     t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id", using: :btree | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue