commit
6599b27b2b
After Width: | Height: | Size: 19 KiB |
@ -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
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue