Merge branch 'main' into glitch-soc/merge-upstream

Conflicts:
- `app/controllers/settings/preferences_controller.rb`:
  Upstream dropping `digest` from notifications emails while we have more
  notification emails settings.
  Removed `digest` from our list while keeping our extra settings.
- `app/javascript/packs/admin.js`:
  Conflicts caused by glitch-soc's theming system.
  Applied the changes to `app/javascript/core/admin.js`.
- `app/views/settings/preferences/other/show.html.haml`:
  Upstream removed a setting close to a glitch-soc-only setting.
  Applied upstream's change.
master
Claire 2022-08-28 11:26:27 +02:00
commit 077183a121
72 changed files with 1796 additions and 830 deletions

View File

@ -75,8 +75,8 @@ GEM
minitest (>= 5.1)
tzinfo (~> 2.0)
zeitwerk (~> 2.3)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
addressable (2.8.1)
public_suffix (>= 2.0.2, < 6.0)
aes_key_wrap (1.1.0)
airbrussh (1.4.1)
sshkit (>= 1.6.1, != 1.7.0)
@ -424,7 +424,7 @@ GEM
concurrent-ruby (~> 1.0, >= 1.0.2)
sidekiq (>= 3.5)
statsd-ruby (~> 1.4, >= 1.4.0)
oj (3.13.20)
oj (3.13.21)
omniauth (1.9.2)
hashie (>= 3.4.6)
rack (>= 1.6.2, < 3)
@ -480,8 +480,8 @@ GEM
pry (>= 0.13, < 0.15)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.7)
puma (5.6.4)
public_suffix (5.0.0)
puma (5.6.5)
nio4r (~> 2.0)
pundit (2.2.0)
activesupport (>= 3.0.0)

View File

@ -16,7 +16,11 @@ module Admin
def batch
authorize :account, :index?
@form = Form::AccountBatch.new(form_account_batch_params.merge(current_account: current_account, action: action_from_button))
@form = Form::AccountBatch.new(form_account_batch_params)
@form.current_account = current_account
@form.action = action_from_button
@form.select_all_matching = params[:select_all_matching]
@form.query = filtered_accounts
@form.save
rescue ActionController::ParameterMissing
flash[:alert] = I18n.t('admin.accounts.no_account_selected')

View File

@ -23,6 +23,7 @@ module Admin
@role.current_account = current_account
if @role.save
log_action :create, @role
redirect_to admin_roles_path
else
render :new
@ -39,6 +40,7 @@ module Admin
@role.current_account = current_account
if @role.update(resource_params)
log_action :update, @role
redirect_to admin_roles_path
else
render :edit
@ -48,6 +50,7 @@ module Admin
def destroy
authorize @role, :destroy?
@role.destroy!
log_action :destroy, @role
redirect_to admin_roles_path
end

View File

@ -14,6 +14,7 @@ module Admin
@user.current_account = current_account
if @user.update(resource_params)
log_action :change_role, @user
redirect_to admin_account_path(@user.account_id), notice: I18n.t('admin.accounts.change_role.changed_msg')
else
render :show

View File

@ -0,0 +1,99 @@
# frozen_string_literal: true
class Api::V1::Admin::CanonicalEmailBlocksController < Api::BaseController
include Authorization
include AccountableConcern
LIMIT = 100
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:canonical_email_blocks' }, only: [:index, :show, :test]
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:canonical_email_blocks' }, except: [:index, :show, :test]
before_action :set_canonical_email_blocks, only: :index
before_action :set_canonical_email_blocks_from_test, only: [:test]
before_action :set_canonical_email_block, only: [:show, :destroy]
after_action :verify_authorized
after_action :insert_pagination_headers, only: :index
PAGINATION_PARAMS = %i(limit).freeze
def index
authorize :canonical_email_block, :index?
render json: @canonical_email_blocks, each_serializer: REST::Admin::CanonicalEmailBlockSerializer
end
def show
authorize @canonical_email_block, :show?
render json: @canonical_email_block, serializer: REST::Admin::CanonicalEmailBlockSerializer
end
def test
authorize :canonical_email_block, :test?
render json: @canonical_email_blocks, each_serializer: REST::Admin::CanonicalEmailBlockSerializer
end
def create
authorize :canonical_email_block, :create?
@canonical_email_block = CanonicalEmailBlock.create!(resource_params)
log_action :create, @canonical_email_block
render json: @canonical_email_block, serializer: REST::Admin::CanonicalEmailBlockSerializer
end
def destroy
authorize @canonical_email_block, :destroy?
@canonical_email_block.destroy!
log_action :destroy, @canonical_email_block
render json: @canonical_email_block, serializer: REST::Admin::CanonicalEmailBlockSerializer
end
private
def resource_params
params.permit(:canonical_email_hash, :email)
end
def set_canonical_email_blocks
@canonical_email_blocks = CanonicalEmailBlock.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def set_canonical_email_blocks_from_test
@canonical_email_blocks = CanonicalEmailBlock.matching_email(params[:email])
end
def set_canonical_email_block
@canonical_email_block = CanonicalEmailBlock.find(params[:id])
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_admin_canonical_email_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
def prev_path
api_v1_admin_canonical_email_blocks_url(pagination_params(min_id: pagination_since_id)) unless @canonical_email_blocks.empty?
end
def pagination_max_id
@canonical_email_blocks.last.id
end
def pagination_since_id
@canonical_email_blocks.first.id
end
def records_continue?
@canonical_email_blocks.size == limit_param(LIMIT)
end
def pagination_params(core_params)
params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params)
end
end

View File

@ -0,0 +1,90 @@
# frozen_string_literal: true
class Api::V1::Admin::EmailDomainBlocksController < Api::BaseController
include Authorization
include AccountableConcern
LIMIT = 100
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:email_domain_blocks' }, only: [:index, :show]
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:email_domain_blocks' }, except: [:index, :show]
before_action :set_email_domain_blocks, only: :index
before_action :set_email_domain_block, only: [:show, :destroy]
after_action :verify_authorized
after_action :insert_pagination_headers, only: :index
PAGINATION_PARAMS = %i(
limit
).freeze
def create
authorize :email_domain_block, :create?
@email_domain_block = EmailDomainBlock.create!(resource_params)
log_action :create, @email_domain_block
render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer
end
def index
authorize :email_domain_block, :index?
render json: @email_domain_blocks, each_serializer: REST::Admin::EmailDomainBlockSerializer
end
def show
authorize @email_domain_block, :show?
render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer
end
def destroy
authorize @email_domain_block, :destroy?
@email_domain_block.destroy!
log_action :destroy, @email_domain_block
render json: @email_domain_block, serializer: REST::Admin::EmailDomainBlockSerializer
end
private
def set_email_domain_blocks
@email_domain_blocks = EmailDomainBlock.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def set_email_domain_block
@email_domain_block = EmailDomainBlock.find(params[:id])
end
def resource_params
params.permit(:domain)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_admin_email_domain_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
def prev_path
api_v1_admin_email_domain_blocks_url(pagination_params(min_id: pagination_since_id)) unless @email_domain_blocks.empty?
end
def pagination_max_id
@email_domain_blocks.last.id
end
def pagination_since_id
@email_domain_blocks.first.id
end
def records_continue?
@email_domain_blocks.size == limit_param(LIMIT)
end
def pagination_params(core_params)
params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params)
end
end

View File

@ -0,0 +1,99 @@
# frozen_string_literal: true
class Api::V1::Admin::IpBlocksController < Api::BaseController
include Authorization
include AccountableConcern
LIMIT = 100
before_action -> { authorize_if_got_token! :'admin:read', :'admin:read:ip_blocks' }, only: [:index, :show]
before_action -> { authorize_if_got_token! :'admin:write', :'admin:write:ip_blocks' }, except: [:index, :show]
before_action :set_ip_blocks, only: :index
before_action :set_ip_block, only: [:show, :update, :destroy]
after_action :verify_authorized
after_action :insert_pagination_headers, only: :index
PAGINATION_PARAMS = %i(
limit
).freeze
def create
authorize :ip_block, :create?
@ip_block = IpBlock.create!(resource_params)
log_action :create, @ip_block
render json: @ip_block, serializer: REST::Admin::IpBlockSerializer
end
def index
authorize :ip_block, :index?
render json: @ip_blocks, each_serializer: REST::Admin::IpBlockSerializer
end
def show
authorize @ip_block, :show?
render json: @ip_block, serializer: REST::Admin::IpBlockSerializer
end
def update
authorize @ip_block, :update?
@ip_block.update(resource_params)
log_action :update, @ip_block
render json: @ip_block, serializer: REST::Admin::IpBlockSerializer
end
def destroy
authorize @ip_block, :destroy?
@ip_block.destroy!
log_action :destroy, @ip_block
render json: @ip_block, serializer: REST::Admin::IpBlockSerializer
end
private
def set_ip_blocks
@ip_blocks = IpBlock.order(id: :desc).to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id))
end
def set_ip_block
@ip_block = IpBlock.find(params[:id])
end
def resource_params
params.permit(:ip, :severity, :comment, :expires_in)
end
def insert_pagination_headers
set_pagination_headers(next_path, prev_path)
end
def next_path
api_v1_admin_ip_blocks_url(pagination_params(max_id: pagination_max_id)) if records_continue?
end
def prev_path
api_v1_admin_ip_blocks_url(pagination_params(min_id: pagination_since_id)) unless @ip_blocks.empty?
end
def pagination_max_id
@ip_blocks.last.id
end
def pagination_since_id
@ip_blocks.first.id
end
def records_continue?
@ip_blocks.size == limit_param(LIMIT)
end
def pagination_params(core_params)
params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params)
end
end

View File

@ -3,7 +3,11 @@
module AccountableConcern
extend ActiveSupport::Concern
def log_action(action, target, options = {})
Admin::ActionLog.create(account: current_account, action: action, target: target, recorded_changes: options.stringify_keys)
def log_action(action, target)
Admin::ActionLog.create(
account: current_account,
action: action,
target: target
)
end
end

View File

@ -58,7 +58,7 @@ class Settings::PreferencesController < Settings::BaseController
:setting_trends,
:setting_crop_images,
:setting_always_send_emails,
notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag trending_link trending_status appeal),
notification_emails: %i(follow follow_request reblog favourite mention report pending_account trending_tag trending_link trending_status appeal),
interactions: %i(must_be_follower must_be_following must_be_following_dm)
)
end

View File

@ -2,64 +2,29 @@
module Admin::ActionLogsHelper
def log_target(log)
if log.target
linkable_log_target(log.target)
else
log_target_from_history(log.target_type, log.recorded_changes)
end
end
private
def linkable_log_target(record)
case record.class.name
case log.target_type
when 'Account'
link_to record.acct, admin_account_path(record.id)
link_to log.human_identifier, admin_account_path(log.target_id)
when 'User'
link_to record.account.acct, admin_account_path(record.account_id)
when 'CustomEmoji'
record.shortcode
link_to log.human_identifier, admin_account_path(log.route_param)
when 'UserRole'
link_to log.human_identifier, admin_roles_path(log.target_id)
when 'Report'
link_to "##{record.id}", admin_report_path(record)
link_to "##{log.human_identifier}", admin_report_path(log.target_id)
when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain'
link_to record.domain, "https://#{record.domain}"
link_to log.human_identifier, "https://#{log.human_identifier}"
when 'Status'
link_to record.account.acct, ActivityPub::TagManager.instance.url_for(record)
link_to log.human_identifier, log.permalink
when 'AccountWarning'
link_to record.target_account.acct, admin_account_path(record.target_account_id)
link_to log.human_identifier, admin_account_path(log.target_id)
when 'Announcement'
link_to truncate(record.text), edit_admin_announcement_path(record.id)
when 'IpBlock'
"#{record.ip}/#{record.ip.prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{record.severity}")})"
when 'Instance'
record.domain
link_to truncate(log.human_identifier), edit_admin_announcement_path(log.target_id)
when 'IpBlock', 'Instance', 'CustomEmoji'
log.human_identifier
when 'CanonicalEmailBlock'
content_tag(:samp, log.human_identifier[0...7], title: log.human_identifier)
when 'Appeal'
link_to record.account.acct, disputes_strike_path(record.strike)
end
end
def log_target_from_history(type, attributes)
case type
when 'User'
attributes['username']
when 'CustomEmoji'
attributes['shortcode']
when 'DomainBlock', 'DomainAllow', 'EmailDomainBlock', 'UnavailableDomain'
link_to attributes['domain'], "https://#{attributes['domain']}"
when 'Status'
tmp_status = Status.new(attributes.except('reblogs_count', 'favourites_count'))
if tmp_status.account
link_to tmp_status.account&.acct || "##{tmp_status.account_id}", admin_account_path(tmp_status.account_id)
else
I18n.t('admin.action_logs.deleted_status')
end
when 'Announcement'
truncate(attributes['text'].is_a?(Array) ? attributes['text'].last : attributes['text'])
when 'IpBlock'
"#{attributes['ip']}/#{attributes['ip'].prefix} (#{I18n.t("simple_form.labels.ip_block.severities.#{attributes['severity']}")})"
when 'Instance'
attributes['domain']
link_to log.human_identifier, disputes_strike_path(log.route_param)
end
end
end

View File

@ -6,18 +6,71 @@ import ready from '../mastodon/ready';
const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]';
const showSelectAll = () => {
const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
selectAllMatchingElement.classList.add('active');
};
const hideSelectAll = () => {
const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
const hiddenField = document.querySelector('#select_all_matching');
const selectedMsg = document.querySelector('.batch-table__select-all .selected');
const notSelectedMsg = document.querySelector('.batch-table__select-all .not-selected');
selectAllMatchingElement.classList.remove('active');
selectedMsg.classList.remove('active');
notSelectedMsg.classList.add('active');
hiddenField.value = '0';
};
delegate(document, '#batch_checkbox_all', 'change', ({ target }) => {
const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
[].forEach.call(document.querySelectorAll(batchCheckboxClassName), (content) => {
content.checked = target.checked;
});
if (selectAllMatchingElement) {
if (target.checked) {
showSelectAll();
} else {
hideSelectAll();
}
}
});
delegate(document, '.batch-table__select-all button', 'click', () => {
const hiddenField = document.querySelector('#select_all_matching');
const active = hiddenField.value === '1';
const selectedMsg = document.querySelector('.batch-table__select-all .selected');
const notSelectedMsg = document.querySelector('.batch-table__select-all .not-selected');
if (active) {
hiddenField.value = '0';
selectedMsg.classList.remove('active');
notSelectedMsg.classList.add('active');
} else {
hiddenField.value = '1';
notSelectedMsg.classList.remove('active');
selectedMsg.classList.add('active');
}
});
delegate(document, batchCheckboxClassName, 'change', () => {
const checkAllElement = document.querySelector('#batch_checkbox_all');
const selectAllMatchingElement = document.querySelector('.batch-table__select-all');
if (checkAllElement) {
checkAllElement.checked = [].every.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
checkAllElement.indeterminate = !checkAllElement.checked && [].some.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked);
if (selectAllMatchingElement) {
if (checkAllElement.checked) {
showSelectAll();
} else {
hideSelectAll();
}
}
}
});

View File

@ -6,7 +6,7 @@ const TimelineHint = ({ resource, url }) => (
<div className='timeline-hint'>
<strong><FormattedMessage id='timeline_hint.remote_resource_not_displayed' defaultMessage='{resource} from other servers are not displayed.' values={{ resource }} /></strong>
<br />
<a href={url} target='_blank'><FormattedMessage id='account.browse_more_on_origin_server' defaultMessage='Browse more on the original profile' /></a>
<a href={url} target='_blank' rel='noopener'><FormattedMessage id='account.browse_more_on_origin_server' defaultMessage='Browse more on the original profile' /></a>
</div>
);

View File

@ -1,9 +1,9 @@
import * as registerPushNotifications from './actions/push_notifications';
import { setupBrowserNotifications } from './actions/notifications';
import { default as Mastodon, store } from './containers/mastodon';
import React from 'react';
import ReactDOM from 'react-dom';
import ready from './ready';
import * as registerPushNotifications from 'mastodon/actions/push_notifications';
import { setupBrowserNotifications } from 'mastodon/actions/notifications';
import Mastodon, { store } from 'mastodon/containers/mastodon';
import ready from 'mastodon/ready';
const perf = require('./performance');
@ -24,10 +24,20 @@ function main() {
ReactDOM.render(<Mastodon {...props} />, mountNode);
store.dispatch(setupBrowserNotifications());
if (process.env.NODE_ENV === 'production') {
// avoid offline in dev mode because it's harder to debug
require('offline-plugin/runtime').install();
store.dispatch(registerPushNotifications.register());
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
import('workbox-window')
.then(({ Workbox }) => {
const wb = new Workbox('/sw.js');
return wb.register();
})
.then(() => {
store.dispatch(registerPushNotifications.register());
})
.catch(err => {
console.error(err);
});
}
perf.stop('main()');
});

View File

@ -1,20 +1,59 @@
// import { freeStorage, storageFreeable } from '../storage/modifier';
import './web_push_notifications';
import { ExpirationPlugin } from 'workbox-expiration';
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst } from 'workbox-strategies';
import { handleNotificationClick, handlePush } from './web_push_notifications';
// function openSystemCache() {
// return caches.open('mastodon-system');
// }
const CACHE_NAME_PREFIX = 'mastodon-';
function openWebCache() {
return caches.open('mastodon-web');
return caches.open(`${CACHE_NAME_PREFIX}web`);
}
function fetchRoot() {
return fetch('/', { credentials: 'include', redirect: 'manual' });
}
// const firefox = navigator.userAgent.match(/Firefox\/(\d+)/);
// const invalidOnlyIfCached = firefox && firefox[1] < 60;
precacheAndRoute(self.__WB_MANIFEST);
registerRoute(
/locale_.*\.js$/,
new CacheFirst({
cacheName: `${CACHE_NAME_PREFIX}locales`,
plugins: [
new ExpirationPlugin({
maxAgeSeconds: 30 * 24 * 60 * 60, // 1 month
maxEntries: 5,
}),
],
}),
);
registerRoute(
({ request }) => request.destination === 'font',
new CacheFirst({
cacheName: `${CACHE_NAME_PREFIX}fonts`,
plugins: [
new ExpirationPlugin({
maxAgeSeconds: 30 * 24 * 60 * 60, // 1 month
maxEntries: 5,
}),
],
}),
);
registerRoute(
({ request }) => ['audio', 'image', 'track', 'video'].includes(request.destination),
new CacheFirst({
cacheName: `m${CACHE_NAME_PREFIX}media`,
plugins: [
new ExpirationPlugin({
maxAgeSeconds: 7 * 24 * 60 * 60, // 1 week
maxEntries: 256,
}),
],
}),
);
// Cause a new version of a registered Service Worker to replace an existing one
// that is already installed, and replace the currently active worker on open pages.
@ -52,26 +91,8 @@ self.addEventListener('fetch', function(event) {
return response;
}));
} /* else if (storageFreeable && (ATTACHMENT_HOST ? url.host === ATTACHMENT_HOST : url.pathname.startsWith('/system/'))) {
event.respondWith(openSystemCache().then(cache => {
return cache.match(event.request.url).then(cached => {
if (cached === undefined) {
const asyncResponse = invalidOnlyIfCached && event.request.cache === 'only-if-cached' ?
fetch(event.request, { cache: 'no-cache' }) : fetch(event.request);
return asyncResponse.then(response => {
if (response.ok) {
cache
.put(event.request.url, response.clone())
.catch(()=>{}).then(freeStorage()).catch();
}
return response;
});
}
return cached;
});
}));
} */
}
});
self.addEventListener('push', handlePush);
self.addEventListener('notificationclick', handleNotificationClick);

View File

@ -75,7 +75,7 @@ const formatMessage = (messageId, locale, values = {}) =>
const htmlToPlainText = html =>
unescape(html.replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n').replace(/<[^>]*>/g, ''));
const handlePush = (event) => {
export const handlePush = (event) => {
const { access_token, notification_id, preferred_locale, title, body, icon } = event.data.json();
// Placeholder until more information can be loaded
@ -189,7 +189,7 @@ const openUrl = url =>
return self.clients.openWindow(url);
});
const handleNotificationClick = (event) => {
export const handleNotificationClick = (event) => {
const reactToNotificationClick = new Promise((resolve, reject) => {
if (event.action) {
if (event.action === 'expand') {
@ -211,6 +211,3 @@ const handleNotificationClick = (event) => {
event.waitUntil(reactToNotificationClick);
};
self.addEventListener('push', handlePush);
self.addEventListener('notificationclick', handleNotificationClick);

View File

@ -1,27 +0,0 @@
export default () => new Promise((resolve, reject) => {
// ServiceWorker is required to synchronize the login state.
// Microsoft Edge 17 does not support getAll according to:
// Catalog of standard and vendor APIs across browsers - Microsoft Edge Development
// https://developer.microsoft.com/en-us/microsoft-edge/platform/catalog/?q=specName%3Aindexeddb
if (!('caches' in self && 'getAll' in IDBObjectStore.prototype)) {
reject();
return;
}
const request = indexedDB.open('mastodon');
request.onerror = reject;
request.onsuccess = ({ target }) => resolve(target.result);
request.onupgradeneeded = ({ target }) => {
const accounts = target.result.createObjectStore('accounts', { autoIncrement: true });
const statuses = target.result.createObjectStore('statuses', { autoIncrement: true });
accounts.createIndex('id', 'id', { unique: true });
accounts.createIndex('moved', 'moved');
statuses.createIndex('id', 'id', { unique: true });
statuses.createIndex('account', 'account');
statuses.createIndex('reblog', 'reblog');
};
});

View File

@ -1,211 +0,0 @@
import openDB from './db';
const accountAssetKeys = ['avatar', 'avatar_static', 'header', 'header_static'];
const storageMargin = 8388608;
const storeLimit = 1024;
// navigator.storage is not present on:
// Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.100 Safari/537.36 Edge/16.16299
// estimate method is not present on Chrome 57.0.2987.98 on Linux.
export const storageFreeable = 'storage' in navigator && 'estimate' in navigator.storage;
function openCache() {
// ServiceWorker and Cache API is not available on iOS 11
// https://webkit.org/status/#specification-service-workers
return self.caches ? caches.open('mastodon-system') : Promise.reject();
}
function printErrorIfAvailable(error) {
if (error) {
console.warn(error);
}
}
function put(name, objects, onupdate, oncreate) {
return openDB().then(db => (new Promise((resolve, reject) => {
const putTransaction = db.transaction(name, 'readwrite');
const putStore = putTransaction.objectStore(name);
const putIndex = putStore.index('id');
objects.forEach(object => {
putIndex.getKey(object.id).onsuccess = retrieval => {
function addObject() {
putStore.add(object);
}
function deleteObject() {
putStore.delete(retrieval.target.result).onsuccess = addObject;
}
if (retrieval.target.result) {
if (onupdate) {
onupdate(object, retrieval.target.result, putStore, deleteObject);
} else {
deleteObject();
}
} else {
if (oncreate) {
oncreate(object, addObject);
} else {
addObject();
}
}
};
});
putTransaction.oncomplete = () => {
const readTransaction = db.transaction(name, 'readonly');
const readStore = readTransaction.objectStore(name);
const count = readStore.count();
count.onsuccess = () => {
const excess = count.result - storeLimit;
if (excess > 0) {
const retrieval = readStore.getAll(null, excess);
retrieval.onsuccess = () => resolve(retrieval.result);
retrieval.onerror = reject;
} else {
resolve([]);
}
};
count.onerror = reject;
};
putTransaction.onerror = reject;
})).then(resolved => {
db.close();
return resolved;
}, error => {
db.close();
throw error;
}));
}
function evictAccountsByRecords(records) {
return openDB().then(db => {
const transaction = db.transaction(['accounts', 'statuses'], 'readwrite');
const accounts = transaction.objectStore('accounts');
const accountsIdIndex = accounts.index('id');
const accountsMovedIndex = accounts.index('moved');
const statuses = transaction.objectStore('statuses');
const statusesIndex = statuses.index('account');
function evict(toEvict) {
toEvict.forEach(record => {
openCache()
.then(cache => accountAssetKeys.forEach(key => cache.delete(records[key])))
.catch(printErrorIfAvailable);
accountsMovedIndex.getAll(record.id).onsuccess = ({ target }) => evict(target.result);
statusesIndex.getAll(record.id).onsuccess =
({ target }) => evictStatusesByRecords(target.result);
accountsIdIndex.getKey(record.id).onsuccess =
({ target }) => target.result && accounts.delete(target.result);
});
}
evict(records);
db.close();
}).catch(printErrorIfAvailable);
}
export function evictStatus(id) {
evictStatuses([id]);
}
export function evictStatuses(ids) {
return openDB().then(db => {
const transaction = db.transaction('statuses', 'readwrite');
const store = transaction.objectStore('statuses');
const idIndex = store.index('id');
const reblogIndex = store.index('reblog');
ids.forEach(id => {
reblogIndex.getAllKeys(id).onsuccess =
({ target }) => target.result.forEach(reblogKey => store.delete(reblogKey));
idIndex.getKey(id).onsuccess =
({ target }) => target.result && store.delete(target.result);
});
db.close();
}).catch(printErrorIfAvailable);
}
function evictStatusesByRecords(records) {
return evictStatuses(records.map(({ id }) => id));
}
export function putAccounts(records, avatarStatic) {
const avatarKey = avatarStatic ? 'avatar_static' : 'avatar';
const newURLs = [];
put('accounts', records, (newRecord, oldKey, store, oncomplete) => {
store.get(oldKey).onsuccess = ({ target }) => {
accountAssetKeys.forEach(key => {
const newURL = newRecord[key];
const oldURL = target.result[key];
if (newURL !== oldURL) {
openCache()
.then(cache => cache.delete(oldURL))
.catch(printErrorIfAvailable);
}
});
const newURL = newRecord[avatarKey];
const oldURL = target.result[avatarKey];
if (newURL !== oldURL) {
newURLs.push(newURL);
}
oncomplete();
};
}, (newRecord, oncomplete) => {
newURLs.push(newRecord[avatarKey]);
oncomplete();
}).then(records => Promise.all([
evictAccountsByRecords(records),
openCache().then(cache => cache.addAll(newURLs)),
])).then(freeStorage, error => {
freeStorage();
throw error;
}).catch(printErrorIfAvailable);
}
export function putStatuses(records) {
put('statuses', records)
.then(evictStatusesByRecords)
.catch(printErrorIfAvailable);
}
export function freeStorage() {
return storageFreeable && navigator.storage.estimate().then(({ quota, usage }) => {
if (usage + storageMargin < quota) {
return null;
}
return openDB().then(db => new Promise((resolve, reject) => {
const retrieval = db.transaction('accounts', 'readonly').objectStore('accounts').getAll(null, 1);
retrieval.onsuccess = () => {
if (retrieval.result.length > 0) {
resolve(evictAccountsByRecords(retrieval.result).then(freeStorage));
} else {
resolve(caches.delete('mastodon-system'));
}
};
retrieval.onerror = reject;
db.close();
}));
});
}

View File

@ -190,6 +190,55 @@ a.table-action-link {
}
}
&__select-all {
background: $ui-base-color;
height: 47px;
align-items: center;
justify-content: center;
border: 1px solid darken($ui-base-color, 8%);
border-top: 0;
color: $secondary-text-color;
display: none;
&.active {
display: flex;
}
.selected,
.not-selected {
display: none;
&.active {
display: block;
}
}
strong {
font-weight: 700;
}
span {
padding: 8px;
display: inline-block;
}
button {
background: transparent;
border: 0;
font: inherit;
color: $highlight-text-color;
border-radius: 4px;
font-weight: 700;
padding: 8px;
&:hover,
&:focus,
&:active {
background: lighten($ui-base-color, 8%);
}
}
}
&__form {
padding: 16px;
border: 1px solid darken($ui-base-color, 8%);

View File

@ -60,7 +60,7 @@ class FeedManager
# @param [Boolean] update
# @return [Boolean]
def push_to_home(account, status, update: false)
return false unless add_to_feed(:home, account.id, status, aggregate_reblogs: account.user&.aggregates_reblogs?)
return false unless add_to_feed(:home, account.id, status, aggregate_reblogs: true)
trim(:home, account.id)
PushUpdateWorker.perform_async(account.id, status.id, "timeline:#{account.id}", { 'update' => update }) if push_update_required?("timeline:#{account.id}")
@ -73,7 +73,7 @@ class FeedManager
# @param [Boolean] update
# @return [Boolean]
def unpush_from_home(account, status, update: false)
return false unless remove_from_feed(:home, account.id, status, aggregate_reblogs: account.user&.aggregates_reblogs?)
return false unless remove_from_feed(:home, account.id, status, aggregate_reblogs: true)
redis.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s)) unless update
true
@ -85,7 +85,7 @@ class FeedManager
# @param [Boolean] update
# @return [Boolean]
def push_to_list(list, status, update: false)
return false if filter_from_list?(status, list) || !add_to_feed(:list, list.id, status, aggregate_reblogs: list.account.user&.aggregates_reblogs?)
return false if filter_from_list?(status, list) || !add_to_feed(:list, list.id, status, aggregate_reblogs: true)
trim(:list, list.id)
PushUpdateWorker.perform_async(list.account_id, status.id, "timeline:list:#{list.id}", { 'update' => update }) if push_update_required?("timeline:list:#{list.id}")
@ -98,7 +98,7 @@ class FeedManager
# @param [Boolean] update
# @return [Boolean]
def unpush_from_list(list, status, update: false)
return false unless remove_from_feed(:list, list.id, status, aggregate_reblogs: list.account.user&.aggregates_reblogs?)
return false unless remove_from_feed(:list, list.id, status, aggregate_reblogs: true)
redis.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s)) unless update
true
@ -133,7 +133,7 @@ class FeedManager
# @return [void]
def merge_into_home(from_account, into_account)
timeline_key = key(:home, into_account.id)
aggregate = into_account.user&.aggregates_reblogs?
aggregate = true
query = from_account.statuses.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4)
if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4
@ -159,7 +159,7 @@ class FeedManager
# @return [void]
def merge_into_list(from_account, list)
timeline_key = key(:list, list.id)
aggregate = list.account.user&.aggregates_reblogs?
aggregate = true
query = from_account.statuses.where(visibility: [:public, :unlisted, :private]).includes(:preloadable_poll, :media_attachments, reblog: :account).limit(FeedManager::MAX_ITEMS / 4)
if redis.zcard(timeline_key) >= FeedManager::MAX_ITEMS / 4
@ -188,7 +188,7 @@ class FeedManager
timeline_status_ids = redis.zrange(timeline_key, 0, -1)
from_account.statuses.select('id, reblog_of_id').where(id: timeline_status_ids).reorder(nil).find_each do |status|
remove_from_feed(:home, into_account.id, status, aggregate_reblogs: into_account.user&.aggregates_reblogs?)
remove_from_feed(:home, into_account.id, status, aggregate_reblogs: true)
end
end
@ -201,7 +201,7 @@ class FeedManager
timeline_status_ids = redis.zrange(timeline_key, 0, -1)
from_account.statuses.select('id, reblog_of_id').where(id: timeline_status_ids).reorder(nil).find_each do |status|
remove_from_feed(:list, list.id, status, aggregate_reblogs: list.account.user&.aggregates_reblogs?)
remove_from_feed(:list, list.id, status, aggregate_reblogs: true)
end
end
@ -260,7 +260,7 @@ class FeedManager
# @return [void]
def populate_home(account)
limit = FeedManager::MAX_ITEMS / 2
aggregate = account.user&.aggregates_reblogs?
aggregate = true
timeline_key = key(:home, account.id)
account.statuses.limit(limit).each do |status|

View File

@ -66,24 +66,6 @@ class NotificationMailer < ApplicationMailer
end
end
def digest(recipient, **opts)
return unless recipient.user.functional?
@me = recipient
@since = opts[:since] || [@me.user.last_emailed_at, (@me.user.current_sign_in_at + 1.day)].compact.max
@notifications_count = Notification.where(account: @me, activity_type: 'Mention').where('created_at > ?', @since).count
return if @notifications_count.zero?
@notifications = Notification.where(account: @me, activity_type: 'Mention').where('created_at > ?', @since).limit(40)
@follows_since = Notification.where(account: @me, activity_type: 'Follow').where('created_at > ?', @since).count
locale_for_account(@me) do
mail to: @me.user.email,
subject: I18n.t(:subject, scope: [:notification_mailer, :digest], count: @notifications_count)
end
end
private
def thread_by_conversation(conversation)

View File

@ -364,6 +364,10 @@ class Account < ApplicationRecord
username
end
def to_log_human_identifier
acct
end
def excluded_from_timeline_account_ids
Rails.cache.fetch("exclude_account_ids_for:#{id}") { block_relationships.pluck(:target_account_id) + blocked_by_relationships.pluck(:account_id) + mute_relationships.pluck(:target_account_id) }
end

View File

@ -43,4 +43,8 @@ class AccountWarning < ApplicationRecord
def overruled?
overruled_at.present?
end
def to_log_human_identifier
target_account.acct
end
end

View File

@ -9,38 +9,42 @@
# action :string default(""), not null
# target_type :string
# target_id :bigint(8)
# recorded_changes :text default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# human_identifier :string
# route_param :string
# permalink :string
#
class Admin::ActionLog < ApplicationRecord
serialize :recorded_changes
self.ignored_columns = %w(
recorded_changes
)
belongs_to :account
belongs_to :target, polymorphic: true, optional: true
default_scope -> { order('id desc') }
before_validation :set_human_identifier
before_validation :set_route_param
before_validation :set_permalink
def action
super.to_sym
end
before_validation :set_changes
private
def set_changes
case action
when :destroy, :create
self.recorded_changes = target.attributes
when :update, :promote, :demote
self.recorded_changes = target.previous_changes
when :change_email
self.recorded_changes = ActiveSupport::HashWithIndifferentAccess.new(
email: [target.email, nil],
unconfirmed_email: [nil, target.unconfirmed_email]
)
end
def set_human_identifier
self.human_identifier = target.to_log_human_identifier if target.respond_to?(:to_log_human_identifier)
end
def set_route_param
self.route_param = target.to_log_route_param if target.respond_to?(:to_log_route_param)
end
def set_permalink
self.permalink = target.to_log_permalink if target.respond_to?(:to_log_permalink)
end
end

View File

@ -12,6 +12,7 @@ class Admin::ActionLogFilter
reject_appeal: { target_type: 'Appeal', action: 'reject' }.freeze,
assigned_to_self_report: { target_type: 'Report', action: 'assigned_to_self' }.freeze,
change_email_user: { target_type: 'User', action: 'change_email' }.freeze,
change_role_user: { target_type: 'User', action: 'change_role' }.freeze,
confirm_user: { target_type: 'User', action: 'confirm' }.freeze,
approve_user: { target_type: 'User', action: 'approve' }.freeze,
reject_user: { target_type: 'User', action: 'reject' }.freeze,
@ -21,16 +22,22 @@ class Admin::ActionLogFilter
create_domain_allow: { target_type: 'DomainAllow', action: 'create' }.freeze,
create_domain_block: { target_type: 'DomainBlock', action: 'create' }.freeze,
create_email_domain_block: { target_type: 'EmailDomainBlock', action: 'create' }.freeze,
create_ip_block: { target_type: 'IpBlock', action: 'create' }.freeze,
create_unavailable_domain: { target_type: 'UnavailableDomain', action: 'create' }.freeze,
create_user_role: { target_type: 'UserRole', action: 'create' }.freeze,
create_canonical_email_block: { target_type: 'CanonicalEmailBlock', action: 'create' }.freeze,
demote_user: { target_type: 'User', action: 'demote' }.freeze,
destroy_announcement: { target_type: 'Announcement', action: 'destroy' }.freeze,
destroy_custom_emoji: { target_type: 'CustomEmoji', action: 'destroy' }.freeze,
destroy_domain_allow: { target_type: 'DomainAllow', action: 'destroy' }.freeze,
destroy_domain_block: { target_type: 'DomainBlock', action: 'destroy' }.freeze,
destroy_ip_block: { target_type: 'IpBlock', action: 'destroy' }.freeze,
destroy_email_domain_block: { target_type: 'EmailDomainBlock', action: 'destroy' }.freeze,
destroy_instance: { target_type: 'Instance', action: 'destroy' }.freeze,
destroy_unavailable_domain: { target_type: 'UnavailableDomain', action: 'destroy' }.freeze,
destroy_status: { target_type: 'Status', action: 'destroy' }.freeze,
destroy_user_role: { target_type: 'UserRole', action: 'destroy' }.freeze,
destroy_canonical_email_block: { target_type: 'CanonicalEmailBlock', action: 'destroy' }.freeze,
disable_2fa_user: { target_type: 'User', action: 'disable' }.freeze,
disable_custom_emoji: { target_type: 'CustomEmoji', action: 'disable' }.freeze,
disable_user: { target_type: 'User', action: 'disable' }.freeze,
@ -52,6 +59,8 @@ class Admin::ActionLogFilter
update_announcement: { target_type: 'Announcement', action: 'update' }.freeze,
update_custom_emoji: { target_type: 'CustomEmoji', action: 'update' }.freeze,
update_status: { target_type: 'Status', action: 'update' }.freeze,
update_user_role: { target_type: 'UserRole', action: 'update' }.freeze,
update_ip_block: { target_type: 'IpBlock', action: 'update' }.freeze,
unblock_email_account: { target_type: 'Account', action: 'unblock_email' }.freeze,
}.freeze

View File

@ -34,6 +34,10 @@ class Announcement < ApplicationRecord
before_validation :set_all_day
before_validation :set_published, on: :create
def to_log_human_identifier
text
end
def publish!
update!(published: true, published_at: Time.now.utc, scheduled_at: nil)
end

View File

@ -52,6 +52,14 @@ class Appeal < ApplicationRecord
update!(rejected_at: Time.now.utc, rejected_by_account: current_account)
end
def to_log_human_identifier
account.acct
end
def to_log_route_param
account_warning_id
end
private
def validate_time_frame

View File

@ -5,27 +5,30 @@
#
# id :bigint(8) not null, primary key
# canonical_email_hash :string default(""), not null
# reference_account_id :bigint(8) not null
# reference_account_id :bigint(8)
# created_at :datetime not null
# updated_at :datetime not null
#