@ -0,0 +1,21 @@ | |||
import PureRenderMixin from 'react-addons-pure-render-mixin'; | |||
const ExtendedVideoPlayer = React.createClass({ | |||
propTypes: { | |||
src: React.PropTypes.string.isRequired | |||
}, | |||
mixins: [PureRenderMixin], | |||
render () { | |||
return ( | |||
<div> | |||
<video src={this.props.src} autoPlay muted loop /> | |||
</div> | |||
); | |||
}, | |||
}); | |||
export default ExtendedVideoPlayer; |
@ -0,0 +1,52 @@ | |||
import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown'; | |||
import EmojiPicker from 'emojione-picker'; | |||
import PureRenderMixin from 'react-addons-pure-render-mixin'; | |||
import { defineMessages, injectIntl } from 'react-intl'; | |||
const messages = defineMessages({ | |||
emoji: { id: 'emoji_button.label', defaultMessage: 'Emoji' } | |||
}); | |||
const settings = { | |||
imageType: 'png', | |||
sprites: false, | |||
imagePathPNG: '/emoji/' | |||
}; | |||
const EmojiPickerDropdown = React.createClass({ | |||
propTypes: { | |||
intl: React.PropTypes.object.isRequired, | |||
onPickEmoji: React.PropTypes.func.isRequired | |||
}, | |||
mixins: [PureRenderMixin], | |||
setRef (c) { | |||
this.dropdown = c; | |||
}, | |||
handleChange (data) { | |||
this.dropdown.hide(); | |||
this.props.onPickEmoji(data); | |||
}, | |||
render () { | |||
const { intl } = this.props; | |||
return ( | |||
<Dropdown ref={this.setRef} style={{ marginLeft: '5px' }}> | |||
<DropdownTrigger className='icon-button' title={intl.formatMessage(messages.emoji)} style={{ fontSize: `24px`, width: `24px`, lineHeight: `24px`, display: 'block', marginLeft: '2px' }}> | |||
<i className={`fa fa-smile-o`} style={{ verticalAlign: 'middle' }} /> | |||
</DropdownTrigger> | |||
<DropdownContent> | |||
<EmojiPicker emojione={settings} onChange={this.handleChange} /> | |||
</DropdownContent> | |||
</Dropdown> | |||
); | |||
} | |||
}); | |||
export default injectIntl(EmojiPickerDropdown); |
@ -0,0 +1,22 @@ | |||
const play = audio => { | |||
if (!audio.paused) { | |||
audio.pause(); | |||
audio.fastSeek(0); | |||
} | |||
audio.play(); | |||
}; | |||
export default function soundsMiddleware() { | |||
const soundCache = { | |||
boop: new Audio(['/sounds/boop.mp3']) | |||
}; | |||
return ({ dispatch }) => next => (action) => { | |||
if (action.meta && action.meta.sound && soundCache[action.meta.sound]) { | |||
play(soundCache[action.meta.sound]); | |||
} | |||
return next(action); | |||
}; | |||
}; |
@ -0,0 +1,27 @@ | |||
// U+0590 to U+05FF - Hebrew | |||
// U+0600 to U+06FF - Arabic | |||
// U+0700 to U+074F - Syriac | |||
// U+0750 to U+077F - Arabic Supplement | |||
// U+0780 to U+07BF - Thaana | |||
// U+07C0 to U+07FF - N'Ko | |||
// U+0800 to U+083F - Samaritan | |||
// U+08A0 to U+08FF - Arabic Extended-A | |||
// U+FB1D to U+FB4F - Hebrew presentation forms | |||
// U+FB50 to U+FDFF - Arabic presentation forms A | |||
// U+FE70 to U+FEFF - Arabic presentation forms B | |||
const rtlChars = /[\u0590-\u083F]|[\u08A0-\u08FF]|[\uFB1D-\uFDFF]|[\uFE70-\uFEFF]/mg; | |||
export function isRtl(text) { | |||
if (text.length === 0) { | |||
return false; | |||
} | |||
const matches = text.match(rtlChars); | |||
if (!matches) { | |||
return false; | |||
} | |||
return matches.length / text.trim().length > 0.3; | |||
}; |
@ -0,0 +1,21 @@ | |||
# frozen_string_literal: true | |||
class Api::V1::MutesController < ApiController | |||
before_action -> { doorkeeper_authorize! :follow } | |||
before_action :require_user! | |||
respond_to :json | |||
def index | |||
results = Mute.where(account: current_account).paginate_by_max_id(limit_param(DEFAULT_ACCOUNTS_LIMIT), params[:max_id], params[:since_id]) | |||
accounts = Account.where(id: results.map(&:target_account_id)).map { |a| [a.id, a] }.to_h | |||
@accounts = results.map { |f| accounts[f.target_account_id] } | |||
set_account_counters_maps(@accounts) | |||
next_path = api_v1_mutes_url(max_id: results.last.id) if results.size == limit_param(DEFAULT_ACCOUNTS_LIMIT) | |||
prev_path = api_v1_mutes_url(since_id: results.first.id) unless results.empty? | |||
set_pagination_headers(next_path, prev_path) | |||
end | |||
end |
@ -0,0 +1,11 @@ | |||
# frozen_string_literal: true | |||
class Mute < ApplicationRecord | |||
include Paginable | |||
belongs_to :account | |||
belongs_to :target_account, class_name: 'Account' | |||
validates :account, :target_account, presence: true | |||
validates :account_id, uniqueness: { scope: :target_account_id } | |||
end |
@ -0,0 +1,23 @@ | |||
# frozen_string_literal: true | |||
class MuteService < BaseService | |||
def call(account, target_account) | |||
return if account.id == target_account.id | |||
clear_home_timeline(account, target_account) | |||
account.mute!(target_account) | |||
end | |||
private | |||
def clear_home_timeline(account, target_account) | |||
home_key = FeedManager.instance.key(:home, account.id) | |||
target_account.statuses.select('id').find_each do |status| | |||
redis.zrem(home_key, status.id) | |||
end | |||
end | |||
def redis | |||
Redis.current | |||
end | |||
end |
@ -0,0 +1,11 @@ | |||
# frozen_string_literal: true | |||
class UnmuteService < BaseService | |||
def call(account, target_account) | |||
return unless account.muting?(target_account) | |||
account.unmute!(target_account) | |||
MergeWorker.perform_async(target_account.id, account.id) if account.following?(target_account) | |||
end | |||
end |
@ -1,5 +1,5 @@ | |||
object @media | |||
attribute :id, :type | |||
node(:url) { |media| full_asset_url(media.file.url( :original)) } | |||
node(:preview_url) { |media| full_asset_url(media.file.url( :small)) } | |||
node(:url) { |media| full_asset_url(media.file.url(:original)) } | |||
node(:preview_url) { |media| full_asset_url(media.file.url(:small)) } | |||
node(:text_url) { |media| medium_url(media) } |
@ -0,0 +1,2 @@ | |||
collection @accounts | |||
extends 'api/v1/accounts/show' |
@ -1,5 +1,5 @@ | |||
<%= yield %> | |||
--- | |||
<%= t('application_mailer.signature', instance: Rails.configuration.x.local_domain) %> | |||
<%= t('application_mailer.settings', link: settings_preferences_url) %> |
@ -1,3 +1,3 @@ | |||
<%= strip_tags(@status.content) %> | |||
<%= raw Formatter.instance.plaintext(status) %> | |||
<%= web_url("statuses/#{@status.id}") %> | |||
<%= raw t('application_mailer.view')%> <%= web_url("statuses/#{status.id}") %> |
@ -0,0 +1,15 @@ | |||
<%= display_name(@me) %>, | |||
<%= raw t('notification_mailer.digest.body', since: @since, instance: root_url) %> | |||
<% @notifications.each do |notification| %> | |||
* <%= raw t('notification_mailer.digest.mention', name: notification.from_account.acct) %> | |||
<%= raw Formatter.instance.plaintext(notification.target_status) %> | |||
<%= raw t('application_mailer.view')%> <%= web_url("statuses/#{notification.target_status.id}") %> | |||
<% end %> | |||
<% if @follows_since > 0 %> | |||
<%= raw t('notification_mailer.digest.new_followers_summary', count: @follows_since) %> | |||
<% end %> |
@ -1,5 +1,5 @@ | |||
<%= display_name(@me) %>, | |||
<%= t('notification_mailer.favourite.body', name: @account.acct) %> | |||
<%= raw t('notification_mailer.favourite.body', name: @account.acct) %> | |||
<%= render partial: 'status' %> | |||
<%= render partial: 'status', locals: { status: @status } %> |
@ -1,5 +1,5 @@ | |||
<%= display_name(@me) %>, | |||
<%= t('notification_mailer.follow.body', name: @account.acct) %> | |||
<%= raw t('notification_mailer.follow.body', name: @account.acct) %> | |||
<%= web_url("accounts/#{@account.id}") %> | |||
<%= raw t('application_mailer.view')%> <%= web_url("accounts/#{@account.id}") %> |
@ -1,5 +1,5 @@ | |||
<%= display_name(@me) %>, | |||
<%= t('notification_mailer.follow_request.body', name: @account.acct) %> | |||
<%= raw t('notification_mailer.follow_request.body', name: @account.acct) %> | |||
<%= web_url("follow_requests") %> | |||
<%= raw t('application_mailer.view')%> <%= web_url("follow_requests") %> |
@ -1,5 +1,5 @@ | |||
<%= display_name(@me) %>, | |||
<%= t('notification_mailer.mention.body', name: @status.account.acct) %> | |||
<%= raw t('notification_mailer.mention.body', name: @status.account.acct) %> | |||
<%= render partial: 'status' %> | |||
<%= render partial: 'status', locals: { status: @status } %> |
@ -1,5 +1,5 @@ | |||
<%= display_name(@me) %>, | |||
<%= t('notification_mailer.reblog.body', name: @account.acct) %> | |||
<%= raw t('notification_mailer.reblog.body', name: @account.acct) %> | |||
<%= render partial: 'status' %> | |||
<%= render partial: 'status', locals: { status: @status } %> |
@ -0,0 +1,4 @@ | |||
.media-item | |||
= link_to media.remote_url.blank? ? media.file.url(:original) : media.remote_url, style: media.image? ? "background-image: url(#{media.file.url(:original)})" : "", target: '_blank', rel: 'noopener', class: "u-#{media.video? || media.gifv? ? 'video' : 'photo'}" do | |||
- unless media.image? | |||
%video{ src: media.file.url(:original), autoplay: true, loop: true }/ |
@ -0,0 +1,14 @@ | |||
# frozen_string_literal: true | |||
class DigestMailerWorker | |||
include Sidekiq::Worker | |||
sidekiq_options queue: 'mailers' | |||
def perform(user_id) | |||
user = User.find(user_id) | |||
return unless user.settings.notification_emails['digest'] | |||
NotificationMailer.digest(user.account).deliver_now! | |||
user.touch(:last_emailed_at) | |||
end | |||
end |
@ -1,6 +1,6 @@ | |||
Rabl.configure do |config| | |||
config.cache_all_output = false | |||
config.cache_sources = !!Rails.env.production? | |||
config.cache_sources = Rails.env.production? | |||
config.include_json_root = false | |||
config.view_paths = [Rails.root.join('app/views')] | |||
end |
@ -0,0 +1,12 @@ | |||
class CreateMutes < ActiveRecord::Migration[5.0] | |||
def change | |||
create_table :mutes do |t| | |||
t.integer :account_id, null: false | |||
t.integer :target_account_id, null: false | |||
t.timestamps null: false | |||
end | |||
add_index :mutes, [:account_id, :target_account_id], unique: true | |||
end | |||
end |
@ -0,0 +1,5 @@ | |||
class AddLastEmailedAtToUsers < ActiveRecord::Migration[5.0] | |||
def change | |||
add_column :users, :last_emailed_at, :datetime, null: true, default: nil | |||
end | |||
end |
@ -0,0 +1,12 @@ | |||
class AddTypeToMediaAttachments < ActiveRecord::Migration[5.0] | |||
def up | |||
add_column :media_attachments, :type, :integer, default: 0, null: false | |||
MediaAttachment.where(file_content_type: MediaAttachment::IMAGE_MIME_TYPES).update_all(type: MediaAttachment.types[:image]) | |||
MediaAttachment.where(file_content_type: MediaAttachment::VIDEO_MIME_TYPES).update_all(type: MediaAttachment.types[:video]) | |||
end | |||
def down | |||
remove_column :media_attachments, :type | |||
end | |||
end |
@ -1,4 +1,4 @@ | |||
Push notifications | |||
================== | |||
**Note: This push notification design turned out to not be fully operational on the side of Firebase. A different approach is in consideration** | |||
See <https://github.com/Gargron/tusky-api> for an example of how to create push notifications for a mobile app. It involves using the Mastodon streaming API on behalf of the app's users, as a sort of proxy. |
@ -0,0 +1,21 @@ | |||
# frozen_string_literal: true | |||
module Paperclip | |||
# This transcoder is only to be used for the MediaAttachment model | |||
# to convert animated gifs to webm | |||
class GifTranscoder < Paperclip::Processor | |||
def make | |||
num_frames = identify('-format %n :file', file: file.path).to_i | |||
return file unless options[:style] == :original && num_frames > 1 | |||
final_file = Paperclip::Transcoder.make(file, options, attachment) | |||
attachment.instance.file_file_name = 'media.mp4' | |||
attachment.instance.file_content_type = 'video/mp4' | |||
attachment.instance.type = MediaAttachment.types[:gifv] | |||
final_file | |||
end | |||
end | |||
end |
@ -0,0 +1,14 @@ | |||
# frozen_string_literal: true | |||
module Paperclip | |||
# This transcoder is only to be used for the MediaAttachment model | |||
# to check when uploaded videos are actually gifv's | |||
class VideoTranscoder < Paperclip::Processor | |||
def make | |||
meta = ::Av.cli.identify(@file.path) | |||
attachment.instance.type = MediaAttachment.types[:gifv] unless meta[:audio_encode] | |||
Paperclip::Transcoder.make(file, options, attachment) | |||
end | |||
end | |||
end |
@ -0,0 +1,19 @@ | |||
require 'rails_helper' | |||
RSpec.describe Api::V1::MutesController, type: :controller do | |||
render_views | |||
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) } | |||
let(:token) { double acceptable?: true, resource_owner_id: user.id } | |||
before do | |||
allow(controller).to receive(:doorkeeper_token) { token } | |||
end | |||
describe 'GET #index' do | |||
it 'returns http success' do | |||
get :index | |||
expect(response).to have_http_status(:success) | |||
end | |||
end | |||
end |
@ -0,0 +1,3 @@ | |||
Fabricator(:mute) do | |||
end |