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
|
object @media
|
||||||
attribute :id, :type
|
attribute :id, :type
|
||||||
node(:url) { |media| full_asset_url(media.file.url( :original)) }
|
node(:url) { |media| full_asset_url(media.file.url(:original)) }
|
||||||
node(:preview_url) { |media| full_asset_url(media.file.url( :small)) }
|
node(:preview_url) { |media| full_asset_url(media.file.url(:small)) }
|
||||||
node(:text_url) { |media| medium_url(media) }
|
node(:text_url) { |media| medium_url(media) }
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
collection @accounts
|
||||||
|
extends 'api/v1/accounts/show'
|
@ -1,5 +1,5 @@
|
|||||||
<%= yield %>
|
<%= yield %>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<%= t('application_mailer.signature', instance: Rails.configuration.x.local_domain) %>
|
<%= 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) %>,
|
<%= 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) %>,
|
<%= 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) %>,
|
<%= 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) %>,
|
<%= 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) %>,
|
<%= 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|
|
Rabl.configure do |config|
|
||||||
config.cache_all_output = false
|
config.cache_all_output = false
|
||||||
config.cache_sources = !!Rails.env.production?
|
config.cache_sources = Rails.env.production?
|
||||||
config.include_json_root = false
|
config.include_json_root = false
|
||||||
config.view_paths = [Rails.root.join('app/views')]
|
config.view_paths = [Rails.root.join('app/views')]
|
||||||
end
|
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
|
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