Merge pull request #1713 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
commit
02133866e6
@ -127,9 +127,18 @@ jobs:
|
||||
- run:
|
||||
command: ./bin/rails tests:migrations:populate_v2
|
||||
name: Populate database with test data
|
||||
- run:
|
||||
command: ./bin/rails db:migrate VERSION=20180514140000
|
||||
name: Run migrations up to v2.4.0
|
||||
- run:
|
||||
command: ./bin/rails tests:migrations:populate_v2_4
|
||||
name: Populate database with test data
|
||||
- run:
|
||||
command: ./bin/rails db:migrate
|
||||
name: Run all remaining migrations
|
||||
- run:
|
||||
command: ./bin/rails tests:migrations:check_database
|
||||
name: Check migration result
|
||||
|
||||
test-two-step-migrations:
|
||||
executor:
|
||||
@ -150,14 +159,25 @@ jobs:
|
||||
- run:
|
||||
command: ./bin/rails tests:migrations:populate_v2
|
||||
name: Populate database with test data
|
||||
- run:
|
||||
command: ./bin/rails db:migrate VERSION=20180514140000
|
||||
name: Run pre-deployment migrations up to v2.4.0
|
||||
environment:
|
||||
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
|
||||
- run:
|
||||
command: ./bin/rails tests:migrations:populate_v2_4
|
||||
name: Populate database with test data
|
||||
- run:
|
||||
command: ./bin/rails db:migrate
|
||||
name: Run all pre-deployment migrations
|
||||
evironment:
|
||||
environment:
|
||||
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
|
||||
- run:
|
||||
command: ./bin/rails db:migrate
|
||||
name: Run all post-deployment remaining migrations
|
||||
- run:
|
||||
command: ./bin/rails tests:migrations:check_database
|
||||
name: Check migration result
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
|
@ -107,7 +107,7 @@ SMTP_SERVER=smtp.mailgun.org
|
||||
SMTP_PORT=587
|
||||
SMTP_LOGIN=
|
||||
SMTP_PASSWORD=
|
||||
SMTP_FROM_ADDRESS=notificatons@example.com
|
||||
SMTP_FROM_ADDRESS=notifications@example.com
|
||||
|
||||
|
||||
# File storage (optional)
|
||||
|
20
CHANGELOG.md
20
CHANGELOG.md
@ -87,7 +87,7 @@ All notable changes to this project will be documented in this file.
|
||||
- Fix suspended accounts statuses being merged back into timelines ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16628))
|
||||
- Fix crash when encountering invalid account fields ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16598))
|
||||
- Fix invalid blurhash handling for remote activities ([noellabo](https://github.com/mastodon/mastodon/pull/16583))
|
||||
- Fix newlines being added to accout notes when an account moves ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16415), [noellabo](https://github.com/mastodon/mastodon/pull/16576))
|
||||
- Fix newlines being added to account notes when an account moves ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16415), [noellabo](https://github.com/mastodon/mastodon/pull/16576))
|
||||
- Fix crash when creating an announcement with links ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16941))
|
||||
- Fix logging out from one browser logging out all other sessions ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/16943))
|
||||
|
||||
@ -420,7 +420,7 @@ All notable changes to this project will be documented in this file.
|
||||
- Fix inefficiency when fetching bookmarks ([akihikodaki](https://github.com/mastodon/mastodon/pull/14674))
|
||||
- Fix inefficiency when fetching favourites ([akihikodaki](https://github.com/mastodon/mastodon/pull/14673))
|
||||
- Fix inefficiency when fetching media-only account timeline ([akihikodaki](https://github.com/mastodon/mastodon/pull/14675))
|
||||
- Fix inefficieny when deleting accounts ([Gargron](https://github.com/mastodon/mastodon/pull/15387), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15409), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15407), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15408), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15402), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15416), [Gargron](https://github.com/mastodon/mastodon/pull/15421))
|
||||
- Fix inefficiency when deleting accounts ([Gargron](https://github.com/mastodon/mastodon/pull/15387), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15409), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15407), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15408), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15402), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/15416), [Gargron](https://github.com/mastodon/mastodon/pull/15421))
|
||||
- Fix redundant query when processing batch actions on custom emojis ([niwatori24](https://github.com/mastodon/mastodon/pull/14534))
|
||||
- Fix slow distinct queries where grouped queries are faster ([Gargron](https://github.com/mastodon/mastodon/pull/15287))
|
||||
- Fix performance on instances list in admin UI ([Gargron](https://github.com/mastodon/mastodon/pull/15282))
|
||||
@ -507,7 +507,7 @@ All notable changes to this project will be documented in this file.
|
||||
- Add blurhash to link previews ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13984), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14143), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13985), [Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/14267), [Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/14278), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14126), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14261), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14260))
|
||||
- In web UI, toots cannot be marked as sensitive unless there is media attached
|
||||
- However, it's possible to do via API or ActivityPub
|
||||
- Thumnails of link previews of such posts now use blurhash in web UI
|
||||
- Thumbnails of link previews of such posts now use blurhash in web UI
|
||||
- The Card entity in REST API has a new `blurhash` attribute
|
||||
- Add support for `summary` field for media description in ActivityPub ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13763))
|
||||
- Add hints about incomplete remote content to web UI ([Gargron](https://github.com/mastodon/mastodon/pull/14031), [noellabo](https://github.com/mastodon/mastodon/pull/14195))
|
||||
@ -530,7 +530,7 @@ All notable changes to this project will be documented in this file.
|
||||
- The `meta` attribute on the Media Attachment entity in REST API can now have a `colors` attribute which in turn contains three hex colors: `background`, `foreground`, and `accent`
|
||||
- The background color is chosen from the most dominant color around the edges of the thumbnail
|
||||
- The foreground and accent colors are chosen from the colors that are the most different from the background color using the CIEDE2000 algorithm
|
||||
- The most satured color of the two is designated as the accent color
|
||||
- The most saturated color of the two is designated as the accent color
|
||||
- The one with the highest W3C contrast is designated as the foreground color
|
||||
- If there are not enough colors in the thumbnail, new ones are generated using a monochrome pattern
|
||||
- Add a visibility indicator to toots in web UI ([noellabo](https://github.com/mastodon/mastodon/pull/14123), [highemerly](https://github.com/mastodon/mastodon/pull/14292))
|
||||
@ -556,7 +556,7 @@ All notable changes to this project will be documented in this file.
|
||||
- Change boost button to no longer serve as visibility indicator in web UI ([noellabo](https://github.com/mastodon/mastodon/pull/14132), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14373))
|
||||
- Change contrast of flash messages ([cchoi12](https://github.com/mastodon/mastodon/pull/13892))
|
||||
- Change wording from "Hide media" to "Hide image/images" in web UI ([ariasuni](https://github.com/mastodon/mastodon/pull/13834))
|
||||
- Change appearence of settings pages to be more consistent ([ariasuni](https://github.com/mastodon/mastodon/pull/13938))
|
||||
- Change appearance of settings pages to be more consistent ([ariasuni](https://github.com/mastodon/mastodon/pull/13938))
|
||||
- Change "Add media" tooltip to not include long list of formats in web UI ([ariasuni](https://github.com/mastodon/mastodon/pull/13954))
|
||||
- Change how badly contrasting emoji are rendered in web UI ([leo60228](https://github.com/mastodon/mastodon/pull/13773), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13772), [mfmfuyu](https://github.com/mastodon/mastodon/pull/14020), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/14015))
|
||||
- Change structure of unavailable content section on about page ([ariasuni](https://github.com/mastodon/mastodon/pull/13930))
|
||||
@ -578,8 +578,8 @@ All notable changes to this project will be documented in this file.
|
||||
### Fixed
|
||||
|
||||
- Fix `following` param not working when exact match is found in account search ([noellabo](https://github.com/mastodon/mastodon/pull/14394))
|
||||
- Fix sometimes occuring duplicate mention notifications ([noellabo](https://github.com/mastodon/mastodon/pull/14378))
|
||||
- Fix RSS feeds not being cachable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14368))
|
||||
- Fix sometimes occurring duplicate mention notifications ([noellabo](https://github.com/mastodon/mastodon/pull/14378))
|
||||
- Fix RSS feeds not being cacheable ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14368))
|
||||
- Fix lack of locking around processing of Announce activities in ActivityPub ([noellabo](https://github.com/mastodon/mastodon/pull/14365))
|
||||
- Fix boosted toots from blocked account not being retroactively removed from TL ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/14339))
|
||||
- Fix large shortened numbers (like 1.2K) using incorrect pluralization ([Sasha-Sorokin](https://github.com/mastodon/mastodon/pull/14061))
|
||||
@ -706,7 +706,7 @@ All notable changes to this project will be documented in this file.
|
||||
- Fix poll refresh button not being debounced in web UI ([rasjonell](https://github.com/mastodon/mastodon/pull/13485), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/13490))
|
||||
- Fix confusing error when failing to add an alias to an unknown account ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13480))
|
||||
- Fix "Email changed" notification sometimes having wrong e-mail ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13475))
|
||||
- Fix varioues issues on the account aliases page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13452))
|
||||
- Fix various issues on the account aliases page ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13452))
|
||||
- Fix API footer link in web UI ([bubblineyuri](https://github.com/mastodon/mastodon/pull/13441))
|
||||
- Fix pagination of following, followers, follow requests, blocks and mutes lists in web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13445))
|
||||
- Fix styling of polls in JS-less fallback on public pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/13436))
|
||||
@ -1496,7 +1496,7 @@ All notable changes to this project will be documented in this file.
|
||||
- Change Docker image to use Ubuntu with jemalloc ([Sir-Boops](https://github.com/mastodon/mastodon/pull/10100), [BenLubar](https://github.com/mastodon/mastodon/pull/10212))
|
||||
- Change public pages to be cacheable by proxies ([BenLubar](https://github.com/mastodon/mastodon/pull/9059))
|
||||
- Change the 410 gone response for suspended accounts to be cacheable by proxies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10339))
|
||||
- Change web UI to not not empty timeline of blocked users on block ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10359))
|
||||
- Change web UI to not empty timeline of blocked users on block ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/10359))
|
||||
- Change JSON serializer to remove unused `@context` values ([Gargron](https://github.com/mastodon/mastodon/pull/10378))
|
||||
- Change GIFV file size limit to be the same as for other videos ([rinsuki](https://github.com/mastodon/mastodon/pull/9924))
|
||||
- Change Webpack to not use @babel/preset-env to compile node_modules ([ykzts](https://github.com/mastodon/mastodon/pull/10289))
|
||||
@ -1673,7 +1673,7 @@ All notable changes to this project will be documented in this file.
|
||||
- Limit maximum visibility of local silenced users to unlisted ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9583))
|
||||
- Change API error message for unconfirmed accounts ([noellabo](https://github.com/mastodon/mastodon/pull/9625))
|
||||
- Change the icon to "reply-all" when it's a reply to other accounts ([mayaeh](https://github.com/mastodon/mastodon/pull/9378))
|
||||
- Do not ignore federated reports targetting already-reported accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9534))
|
||||
- Do not ignore federated reports targeting already-reported accounts ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/9534))
|
||||
- Upgrade default Ruby version to 2.6.0 ([Gargron](https://github.com/mastodon/mastodon/pull/9688))
|
||||
- Change e-mail digest frequency ([Gargron](https://github.com/mastodon/mastodon/pull/9689))
|
||||
- Change Docker images for Tor support in docker-compose.yml ([Sir-Boops](https://github.com/mastodon/mastodon/pull/9438))
|
||||
|
@ -62,7 +62,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
|
||||
return unless page_requested?
|
||||
|
||||
@statuses = cache_collection_paginated_by_id(
|
||||
@account.statuses.permitted_for(@account, signed_request_account),
|
||||
AccountStatusesFilter.new(@account, signed_request_account).results,
|
||||
Status,
|
||||
LIMIT,
|
||||
params_slice(:max_id, :min_id, :since_id)
|
||||
|
@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Accounts::FamiliarFollowersController < Api::BaseController
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:follows' }
|
||||
before_action :require_user!
|
||||
before_action :set_accounts
|
||||
|
||||
def index
|
||||
render json: familiar_followers.accounts, each_serializer: REST::FamiliarFollowersSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_accounts
|
||||
@accounts = Account.without_suspended.where(id: account_ids).select('id, hide_collections').index_by(&:id).values_at(*account_ids).compact
|
||||
end
|
||||
|
||||
def familiar_followers
|
||||
FamiliarFollowersPresenter.new(@accounts, current_user.account_id)
|
||||
end
|
||||
|
||||
def account_ids
|
||||
Array(params[:id]).map(&:to_i)
|
||||
end
|
||||
end
|
@ -22,53 +22,16 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
|
||||
end
|
||||
|
||||
def cached_account_statuses
|
||||
statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses
|
||||
|
||||
statuses.merge!(only_media_scope) if truthy_param?(:only_media)
|
||||
statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
|
||||
statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
|
||||
statuses.merge!(hashtag_scope) if params[:tagged].present?
|
||||
|
||||
cache_collection_paginated_by_id(
|
||||
statuses,
|
||||
AccountStatusesFilter.new(@account, current_account, params).results,
|
||||
Status,
|
||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||
params_slice(:max_id, :since_id, :min_id)
|
||||
)
|
||||
end
|
||||
|
||||
def permitted_account_statuses
|
||||
@account.statuses.permitted_for(@account, current_account)
|
||||
end
|
||||
|
||||
def only_media_scope
|
||||
Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id)
|
||||
end
|
||||
|
||||
def pinned_scope
|
||||
@account.pinned_statuses.permitted_for(@account, current_account)
|
||||
end
|
||||
|
||||
def no_replies_scope
|
||||
Status.without_replies
|
||||
end
|
||||
|
||||
def no_reblogs_scope
|
||||
Status.without_reblogs
|
||||
end
|
||||
|
||||
def hashtag_scope
|
||||
tag = Tag.find_normalized(params[:tagged])
|
||||
|
||||
if tag
|
||||
Status.tagged_with(tag.id)
|
||||
else
|
||||
Status.none
|
||||
end
|
||||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
params.slice(:limit, :only_media, :exclude_replies).permit(:limit, :only_media, :exclude_replies).merge(core_params)
|
||||
params.slice(:limit, *AccountStatusesFilter::KEYS).permit(:limit, *AccountStatusesFilter::KEYS).merge(core_params)
|
||||
end
|
||||
|
||||
def insert_pagination_headers
|
||||
|
@ -16,13 +16,13 @@ class FollowerAccountsController < ApplicationController
|
||||
use_pack 'public'
|
||||
expires_in 0, public: true unless user_signed_in?
|
||||
|
||||
next if @account.user_hides_network?
|
||||
next if @account.hide_collections?
|
||||
|
||||
follows
|
||||
end
|
||||
|
||||
format.json do
|
||||
raise Mastodon::NotPermittedError if page_requested? && @account.user_hides_network?
|
||||
raise Mastodon::NotPermittedError if page_requested? && @account.hide_collections?
|
||||
|
||||
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
|
||||
|
||||
@ -83,7 +83,7 @@ class FollowerAccountsController < ApplicationController
|
||||
end
|
||||
|
||||
def restrict_fields_to
|
||||
if page_requested? || !@account.user_hides_network?
|
||||
if page_requested? || !@account.hide_collections?
|
||||
# Return all fields
|
||||
else
|
||||
%i(id type total_items)
|
||||
|
@ -16,13 +16,13 @@ class FollowingAccountsController < ApplicationController
|
||||
use_pack 'public'
|
||||
expires_in 0, public: true unless user_signed_in?
|
||||
|
||||
next if @account.user_hides_network?
|
||||
next if @account.hide_collections?
|
||||
|
||||
follows
|
||||
end
|
||||
|
||||
format.json do
|
||||
raise Mastodon::NotPermittedError if page_requested? && @account.user_hides_network?
|
||||
raise Mastodon::NotPermittedError if page_requested? && @account.hide_collections?
|
||||
|
||||
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?)
|
||||
|
||||
@ -83,7 +83,7 @@ class FollowingAccountsController < ApplicationController
|
||||
end
|
||||
|
||||
def restrict_fields_to
|
||||
if page_requested? || !@account.user_hides_network?
|
||||
if page_requested? || !@account.hide_collections?
|
||||
# Return all fields
|
||||
else
|
||||
%i(id type total_items)
|
||||
|
@ -48,7 +48,6 @@ class Settings::PreferencesController < Settings::BaseController
|
||||
:setting_system_font_ui,
|
||||
:setting_system_emoji_font,
|
||||
:setting_noindex,
|
||||
:setting_hide_network,
|
||||
:setting_hide_followers_count,
|
||||
:setting_aggregate_reblogs,
|
||||
:setting_show_application,
|
||||
|
@ -20,7 +20,7 @@ class Settings::ProfilesController < Settings::BaseController
|
||||
private
|
||||
|
||||
def account_params
|
||||
params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, fields_attributes: [:name, :value])
|
||||
params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, :bot, :discoverable, :hide_collections, fields_attributes: [:name, :value])
|
||||
end
|
||||
|
||||
def set_account
|
||||
|
@ -144,7 +144,7 @@ class ScrollableList extends PureComponent {
|
||||
this.attachIntersectionObserver();
|
||||
attachFullscreenListener(this.onFullScreenChange);
|
||||
|
||||
// Handle initial scroll posiiton
|
||||
// Handle initial scroll position
|
||||
this.handleScroll();
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,7 @@ export default class MediaContainer extends PureComponent {
|
||||
|
||||
handleOpenVideo = (options) => {
|
||||
const { components } = this.props;
|
||||
const { media } = JSON.parse(components[options.componetIndex].getAttribute('data-props'));
|
||||
const { media } = JSON.parse(components[options.componentIndex].getAttribute('data-props'));
|
||||
const mediaList = fromJS(media);
|
||||
|
||||
document.body.classList.add('with-modals--active');
|
||||
@ -87,7 +87,7 @@ export default class MediaContainer extends PureComponent {
|
||||
...(hashtag ? { hashtag: fromJS(hashtag) } : {}),
|
||||
|
||||
...(componentName === 'Video' ? {
|
||||
componetIndex: i,
|
||||
componentIndex: i,
|
||||
onOpenVideo: this.handleOpenVideo,
|
||||
} : {
|
||||
onOpenMedia: this.handleOpenMedia,
|
||||
|
@ -7,31 +7,28 @@ import { makeGetAccount } from 'flavours/glitch/selectors';
|
||||
import Avatar from 'flavours/glitch/components/avatar';
|
||||
import DisplayName from 'flavours/glitch/components/display_name';
|
||||
import Permalink from 'flavours/glitch/components/permalink';
|
||||
import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
|
||||
import IconButton from 'flavours/glitch/components/icon_button';
|
||||
import Button from 'flavours/glitch/components/button';
|
||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||
import { autoPlayGif, me, unfollowModal } from 'flavours/glitch/util/initial_state';
|
||||
import ShortNumber from 'flavours/glitch/components/short_number';
|
||||
import {
|
||||
followAccount,
|
||||
unfollowAccount,
|
||||
blockAccount,
|
||||
unblockAccount,
|
||||
unmuteAccount,
|
||||
} from 'flavours/glitch/actions/accounts';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import { initMuteModal } from 'flavours/glitch/actions/mutes';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const messages = defineMessages({
|
||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||
unfollowConfirm: {
|
||||
id: 'confirmations.unfollow.confirm',
|
||||
defaultMessage: 'Unfollow',
|
||||
},
|
||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||
cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Cancel follow request' },
|
||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
|
||||
unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
|
||||
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
|
||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
@ -75,18 +72,15 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
onBlock(account) {
|
||||
if (account.getIn(['relationship', 'blocking'])) {
|
||||
dispatch(unblockAccount(account.get('id')));
|
||||
} else {
|
||||
dispatch(blockAccount(account.get('id')));
|
||||
}
|
||||
},
|
||||
|
||||
onMute(account) {
|
||||
if (account.getIn(['relationship', 'muting'])) {
|
||||
dispatch(unmuteAccount(account.get('id')));
|
||||
} else {
|
||||
dispatch(initMuteModal(account));
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default
|
||||
@ -138,69 +132,37 @@ class AccountCard extends ImmutablePureComponent {
|
||||
|
||||
handleMute = () => {
|
||||
this.props.onMute(this.props.account);
|
||||
};
|
||||
}
|
||||
|
||||
handleEditProfile = () => {
|
||||
window.open('/settings/profile', '_blank');
|
||||
}
|
||||
|
||||
render() {
|
||||
const { account, intl } = this.props;
|
||||
|
||||
let buttons;
|
||||
let actionBtn;
|
||||
|
||||
if (
|
||||
account.get('id') !== me &&
|
||||
account.get('relationship', null) !== null
|
||||
) {
|
||||
const following = account.getIn(['relationship', 'following']);
|
||||
const requested = account.getIn(['relationship', 'requested']);
|
||||
const blocking = account.getIn(['relationship', 'blocking']);
|
||||
const muting = account.getIn(['relationship', 'muting']);
|
||||
|
||||
if (requested) {
|
||||
buttons = (
|
||||
<IconButton
|
||||
disabled
|
||||
icon='hourglass'
|
||||
title={intl.formatMessage(messages.requested)}
|
||||
/>
|
||||
);
|
||||
} else if (blocking) {
|
||||
buttons = (
|
||||
<IconButton
|
||||
active
|
||||
icon='unlock'
|
||||
title={intl.formatMessage(messages.unblock, {
|
||||
name: account.get('username'),
|
||||
})}
|
||||
onClick={this.handleBlock}
|
||||
/>
|
||||
);
|
||||
} else if (muting) {
|
||||
buttons = (
|
||||
<IconButton
|
||||
active
|
||||
icon='volume-up'
|
||||
title={intl.formatMessage(messages.unmute, {
|
||||
name: account.get('username'),
|
||||
})}
|
||||
onClick={this.handleMute}
|
||||
/>
|
||||
);
|
||||
} else if (!account.get('moved') || following) {
|
||||
buttons = (
|
||||
<IconButton
|
||||
icon={following ? 'user-times' : 'user-plus'}
|
||||
title={intl.formatMessage(
|
||||
following ? messages.unfollow : messages.follow,
|
||||
)}
|
||||
onClick={this.handleFollow}
|
||||
active={following}
|
||||
/>
|
||||
);
|
||||
if (me !== account.get('id')) {
|
||||
if (!account.get('relationship')) { // Wait until the relationship is loaded
|
||||
actionBtn = '';
|
||||
} else if (account.getIn(['relationship', 'requested'])) {
|
||||
actionBtn = <Button className={classNames('logo-button')} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.handleFollow} />;
|
||||
} else if (account.getIn(['relationship', 'muting'])) {
|
||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unmute)} onClick={this.handleMute} />;
|
||||
} else if (!account.getIn(['relationship', 'blocking'])) {
|
||||
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
|
||||
} else if (account.getIn(['relationship', 'blocking'])) {
|
||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
|
||||
}
|
||||
} else {
|
||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.handleEditProfile} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='directory__card'>
|
||||
<div className='directory__card__img'>
|
||||
<div className='account-card'>
|
||||
<Permalink href={account.get('url')} to={`/@${account.get('acct')}`} className='account-card__permalink'>
|
||||
<div className='account-card__header'>
|
||||
<img
|
||||
src={
|
||||
autoPlayGif ? account.get('header') : account.get('header_static')
|
||||
@ -209,36 +171,31 @@ class AccountCard extends ImmutablePureComponent {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='directory__card__bar'>
|
||||
<Permalink
|
||||
className='directory__card__bar__name'
|
||||
href={account.get('url')}
|
||||
to={`/@${account.get('acct')}`}
|
||||
>
|
||||
<Avatar account={account} size={48} />
|
||||
<div className='account-card__title'>
|
||||
<div className='account-card__title__avatar'><Avatar account={account} size={56} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
</Permalink>
|
||||
|
||||
<div className='directory__card__bar__relationship account__relationship'>
|
||||
{buttons}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='directory__card__extra' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||
{account.get('note').length > 0 && (
|
||||
<div
|
||||
className='account__header__content translate'
|
||||
className='account-card__bio translate'
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='directory__card__extra'>
|
||||
<div className='accounts-table__count'>
|
||||
<div className='account-card__actions'>
|
||||
<div className='account-card__counters'>
|
||||
<div className='account-card__counters__item'>
|
||||
<ShortNumber value={account.get('statuses_count')} />
|
||||
<small>
|
||||
<FormattedMessage id='account.posts' defaultMessage='Toots' />
|
||||
</small>
|
||||
</div>
|
||||
<div className='accounts-table__count'>
|
||||
|
||||
<div className='account-card__counters__item'>
|
||||
{account.get('followers_count') < 0 ? '-' : <ShortNumber value={account.get('followers_count')} />}{' '}
|
||||
<small>
|
||||
<FormattedMessage
|
||||
@ -247,23 +204,22 @@ class AccountCard extends ImmutablePureComponent {
|
||||
/>
|
||||
</small>
|
||||
</div>
|
||||
<div className='accounts-table__count'>
|
||||
{account.get('last_status_at') === null ? (
|
||||
<FormattedMessage
|
||||
id='account.never_active'
|
||||
defaultMessage='Never'
|
||||
/>
|
||||
) : (
|
||||
<RelativeTimestamp timestamp={account.get('last_status_at')} />
|
||||
)}{' '}
|
||||
|
||||
<div className='account-card__counters__item'>
|
||||
<ShortNumber value={account.get('following_count')} />{' '}
|
||||
<small>
|
||||
<FormattedMessage
|
||||
id='account.last_status'
|
||||
defaultMessage='Last active'
|
||||
id='account.following'
|
||||
defaultMessage='Following'
|
||||
/>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='account-card__actions__button'>
|
||||
{actionBtn}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -10,9 +10,9 @@ import { fetchDirectory, expandDirectory } from 'flavours/glitch/actions/directo
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import AccountCard from './components/account_card';
|
||||
import RadioButton from 'flavours/glitch/components/radio_button';
|
||||
import classNames from 'classnames';
|
||||
import LoadMore from 'flavours/glitch/components/load_more';
|
||||
import ScrollContainer from 'flavours/glitch/containers/scroll_container';
|
||||
import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
|
||||
@ -129,7 +129,7 @@ class Directory extends React.PureComponent {
|
||||
const pinned = !!columnId;
|
||||
|
||||
const scrollableArea = (
|
||||
<div className='scrollable' style={{ background: 'transparent' }}>
|
||||
<div className='scrollable'>
|
||||
<div className='filter-form'>
|
||||
<div className='filter-form__column' role='group'>
|
||||
<RadioButton name='order' value='active' label={intl.formatMessage(messages.recentlyActive)} checked={order === 'active'} onChange={this.handleChangeOrder} />
|
||||
@ -142,8 +142,10 @@ class Directory extends React.PureComponent {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={classNames('directory__list', { loading: isLoading })}>
|
||||
{accountIds.map(accountId => <AccountCard id={accountId} key={accountId} />)}
|
||||
<div className='directory__list'>
|
||||
{isLoading ? <LoadingIndicator /> : accountIds.map(accountId => (
|
||||
<AccountCard id={accountId} key={accountId} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<LoadMore onClick={this.handleLoadMore} visible={!isLoading} />
|
||||
|
@ -8,7 +8,7 @@ const messages = defineMessages({
|
||||
dislike: { id: 'report.reasons.dislike', defaultMessage: 'I don\'t like it' },
|
||||
dislike_description: { id: 'report.reasons.dislike_description', defaultMessage: 'It is not something you want to see' },
|
||||
spam: { id: 'report.reasons.spam', defaultMessage: 'It\'s spam' },
|
||||
spam_description: { id: 'report.reasons.spam_description', defaultMessage: 'Malicious links, fake engagement, or repetetive replies' },
|
||||
spam_description: { id: 'report.reasons.spam_description', defaultMessage: 'Malicious links, fake engagement, or repetitive replies' },
|
||||
violation: { id: 'report.reasons.violation', defaultMessage: 'It violates server rules' },
|
||||
violation_description: { id: 'report.reasons.violation_description', defaultMessage: 'You are aware that it breaks specific rules' },
|
||||
other: { id: 'report.reasons.other', defaultMessage: 'It\'s something else' },
|
||||
|
@ -123,7 +123,7 @@ class Video extends React.PureComponent {
|
||||
autoPlay: PropTypes.bool,
|
||||
volume: PropTypes.number,
|
||||
muted: PropTypes.bool,
|
||||
componetIndex: PropTypes.number,
|
||||
componentIndex: PropTypes.number,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -516,7 +516,7 @@ class Video extends React.PureComponent {
|
||||
startTime: this.video.currentTime,
|
||||
autoPlay: !this.state.paused,
|
||||
defaultVolume: this.state.volume,
|
||||
componetIndex: this.props.componetIndex,
|
||||
componentIndex: this.props.componentIndex,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1236,6 +1236,11 @@ a.sparkline {
|
||||
background: $ui-base-color;
|
||||
border-radius: 4px;
|
||||
|
||||
&__permalink {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&__header {
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
@ -1252,20 +1257,22 @@ a.sparkline {
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin-top: -25px;
|
||||
margin-top: -(15px + 8px);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
&__avatar {
|
||||
padding: 15px;
|
||||
padding: 14px;
|
||||
|
||||
img {
|
||||
img,
|
||||
.account__avatar {
|
||||
display: block;
|
||||
margin: 0;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: darken($ui-base-color, 8%);
|
||||
background-color: darken($ui-base-color, 8%);
|
||||
border-radius: 8px;
|
||||
border: 1px solid $ui-base-color;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1273,30 +1280,34 @@ a.sparkline {
|
||||
color: $darker-text-color;
|
||||
padding-bottom: 15px;
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
|
||||
bdi {
|
||||
display: block;
|
||||
color: $primary-text-color;
|
||||
font-weight: 500;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__bio {
|
||||
padding: 0 15px;
|
||||
margin: 8px 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-wrap: break-word;
|
||||
max-height: 18px * 2;
|
||||
max-height: 21px * 2;
|
||||
position: relative;
|
||||
font-size: 15px;
|
||||
line-height: 21px;
|
||||
|
||||
&::after {
|
||||
display: block;
|
||||
content: "";
|
||||
width: 50px;
|
||||
height: 18px;
|
||||
height: 21px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
bottom: 8px;
|
||||
right: 15px;
|
||||
background: linear-gradient(to left, $ui-base-color, transparent);
|
||||
pointer-events: none;
|
||||
@ -1309,10 +1320,6 @@ a.sparkline {
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
|
||||
.fa {
|
||||
color: lighten($dark-text-color, 7%);
|
||||
}
|
||||
}
|
||||
|
||||
&.mention {
|
||||
@ -1329,12 +1336,21 @@ a.sparkline {
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 10px;
|
||||
|
||||
&__button {
|
||||
flex: 0 0 auto;
|
||||
flex-shrink: 1;
|
||||
padding: 0 15px;
|
||||
overflow: hidden;
|
||||
|
||||
.button {
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1343,19 +1359,23 @@ a.sparkline {
|
||||
display: grid;
|
||||
grid-auto-columns: minmax(0, 1fr);
|
||||
grid-auto-flow: column;
|
||||
max-width: 340px;
|
||||
min-width: 65px * 3;
|
||||
|
||||
&__item {
|
||||
padding: 15px;
|
||||
padding: 15px 0;
|
||||
text-align: center;
|
||||
color: $primary-text-color;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
line-height: 21px;
|
||||
|
||||
small {
|
||||
display: block;
|
||||
color: $darker-text-color;
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,133 +1,17 @@
|
||||
.directory {
|
||||
&__list {
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
transition: opacity 100ms ease-in;
|
||||
|
||||
&.loading {
|
||||
opacity: 0.7;
|
||||
.scrollable .account-card {
|
||||
margin: 10px;
|
||||
background: lighten($ui-base-color, 8%);
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
margin: 0;
|
||||
.scrollable .account-card__title__avatar {
|
||||
img,
|
||||
.account__avatar {
|
||||
border-color: lighten($ui-base-color, 8%);
|
||||
}
|
||||
}
|
||||
|
||||
&__card {
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&__img {
|
||||
height: 125px;
|
||||
position: relative;
|
||||
background: darken($ui-base-color, 12%);
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
&__bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: lighten($ui-base-color, 4%);
|
||||
padding: 10px;
|
||||
|
||||
&__name {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__relationship {
|
||||
width: 23px;
|
||||
min-height: 1px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
flex: 0 0 auto;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
padding-top: 2px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
margin: 0;
|
||||
border-radius: 4px;
|
||||
background: darken($ui-base-color, 8%);
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.display-name {
|
||||
margin-left: 15px;
|
||||
text-align: left;
|
||||
|
||||
strong {
|
||||
font-size: 15px;
|
||||
color: $primary-text-color;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: $darker-text-color;
|
||||
font-weight: 400;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__extra {
|
||||
background: $ui-base-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.accounts-table__count {
|
||||
width: 33.33%;
|
||||
flex: 0 0 auto;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.account__header__content {
|
||||
box-sizing: border-box;
|
||||
padding: 15px 10px;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
width: 100%;
|
||||
min-height: 18px + 30px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
p {
|
||||
display: none;
|
||||
|
||||
&:first-child {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
br {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.scrollable .account-card__bio::after {
|
||||
background: linear-gradient(to left, lighten($ui-base-color, 8%), transparent);
|
||||
}
|
||||
|
||||
.filter-form {
|
||||
@ -135,6 +19,7 @@
|
||||
|
||||
&__column {
|
||||
padding: 10px 15px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.radio-button {
|
||||
|
@ -41,7 +41,7 @@
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-family: inherit;
|
||||
font-size: 17px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
line-height: 22px;
|
||||
|
@ -94,17 +94,7 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.directory__list {
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
grid-template-columns: minmax(0, 50%) minmax(0, 50%);
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.directory__card {
|
||||
.account-card {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
@ -411,14 +411,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.directory__card {
|
||||
border-radius: 4px;
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.page-header {
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
border-bottom: 0;
|
||||
@ -841,17 +833,19 @@
|
||||
grid-gap: 10px;
|
||||
grid-template-columns: minmax(0, 50%) minmax(0, 50%);
|
||||
|
||||
.account-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
font-size: 18px;
|
||||
.account-card {
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.directory__card {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
|
@ -75,7 +75,7 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.autossugest-input {
|
||||
.autosuggest-input {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
|
@ -12,11 +12,6 @@ body.rtl {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.directory__card__bar .display-name {
|
||||
margin-left: 0;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.display-name {
|
||||
text-align: right;
|
||||
}
|
||||
|
@ -151,7 +151,7 @@ class ScrollableList extends PureComponent {
|
||||
|
||||
attachFullscreenListener(this.onFullScreenChange);
|
||||
|
||||
// Handle initial scroll posiiton
|
||||
// Handle initial scroll position
|
||||
this.handleScroll();
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,7 @@ export default class MediaContainer extends PureComponent {
|
||||
|
||||
handleOpenVideo = (options) => {
|
||||
const { components } = this.props;
|
||||
const { media } = JSON.parse(components[options.componetIndex].getAttribute('data-props'));
|
||||
const { media } = JSON.parse(components[options.componentIndex].getAttribute('data-props'));
|
||||
const mediaList = fromJS(media);
|
||||
|
||||
document.body.classList.add('with-modals--active');
|
||||
@ -87,7 +87,7 @@ export default class MediaContainer extends PureComponent {
|
||||
...(hashtag ? { hashtag: fromJS(hashtag) } : {}),
|
||||
|
||||
...(componentName === 'Video' ? {
|
||||
componetIndex: i,
|
||||
componentIndex: i,
|
||||
onOpenVideo: this.handleOpenVideo,
|
||||
} : {
|
||||
onOpenMedia: this.handleOpenMedia,
|
||||
|
@ -7,31 +7,28 @@ import { makeGetAccount } from 'mastodon/selectors';
|
||||
import Avatar from 'mastodon/components/avatar';
|
||||
import DisplayName from 'mastodon/components/display_name';
|
||||
import Permalink from 'mastodon/components/permalink';
|
||||
import RelativeTimestamp from 'mastodon/components/relative_timestamp';
|
||||
import IconButton from 'mastodon/components/icon_button';
|
||||
import Button from 'mastodon/components/button';
|
||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||
import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state';
|
||||
import ShortNumber from 'mastodon/components/short_number';
|
||||
import {
|
||||
followAccount,
|
||||
unfollowAccount,
|
||||
blockAccount,
|
||||
unblockAccount,
|
||||
unmuteAccount,
|
||||
} from 'mastodon/actions/accounts';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { initMuteModal } from 'mastodon/actions/mutes';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const messages = defineMessages({
|
||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
|
||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||
unfollowConfirm: {
|
||||
id: 'confirmations.unfollow.confirm',
|
||||
defaultMessage: 'Unfollow',
|
||||
},
|
||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||
cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Cancel follow request' },
|
||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
|
||||
unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
|
||||
unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
|
||||
unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
|
||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
@ -75,18 +72,15 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
onBlock(account) {
|
||||
if (account.getIn(['relationship', 'blocking'])) {
|
||||
dispatch(unblockAccount(account.get('id')));
|
||||
} else {
|
||||
dispatch(blockAccount(account.get('id')));
|
||||
}
|
||||
},
|
||||
|
||||
onMute(account) {
|
||||
if (account.getIn(['relationship', 'muting'])) {
|
||||
dispatch(unmuteAccount(account.get('id')));
|
||||
} else {
|
||||
dispatch(initMuteModal(account));
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default
|
||||
@ -138,69 +132,37 @@ class AccountCard extends ImmutablePureComponent {
|
||||
|
||||
handleMute = () => {
|
||||
this.props.onMute(this.props.account);
|
||||
};
|
||||
}
|
||||
|
||||
handleEditProfile = () => {
|
||||
window.open('/settings/profile', '_blank');
|
||||
}
|
||||
|
||||
render() {
|
||||
const { account, intl } = this.props;
|
||||
|
||||
let buttons;
|
||||
let actionBtn;
|
||||
|
||||
if (
|
||||
account.get('id') !== me &&
|
||||
account.get('relationship', null) !== null
|
||||
) {
|
||||
const following = account.getIn(['relationship', 'following']);
|
||||
const requested = account.getIn(['relationship', 'requested']);
|
||||
const blocking = account.getIn(['relationship', 'blocking']);
|
||||
const muting = account.getIn(['relationship', 'muting']);
|
||||
|
||||
if (requested) {
|
||||
buttons = (
|
||||
<IconButton
|
||||
disabled
|
||||
icon='hourglass'
|
||||
title={intl.formatMessage(messages.requested)}
|
||||
/>
|
||||
);
|
||||
} else if (blocking) {
|
||||
buttons = (
|
||||
<IconButton
|
||||
active
|
||||
icon='unlock'
|
||||
title={intl.formatMessage(messages.unblock, {
|
||||
name: account.get('username'),
|
||||
})}
|
||||
onClick={this.handleBlock}
|
||||
/>
|
||||
);
|
||||
} else if (muting) {
|
||||
buttons = (
|
||||
<IconButton
|
||||
active
|
||||
icon='volume-up'
|
||||
title={intl.formatMessage(messages.unmute, {
|
||||
name: account.get('username'),
|
||||
})}
|
||||
onClick={this.handleMute}
|
||||
/>
|
||||
);
|
||||
} else if (!account.get('moved') || following) {
|
||||
buttons = (
|
||||
<IconButton
|
||||
icon={following ? 'user-times' : 'user-plus'}
|
||||
title={intl.formatMessage(
|
||||
following ? messages.unfollow : messages.follow,
|
||||
)}
|
||||
onClick={this.handleFollow}
|
||||
active={following}
|
||||
/>
|
||||
);
|
||||
if (me !== account.get('id')) {
|
||||
if (!account.get('relationship')) { // Wait until the relationship is loaded
|
||||
actionBtn = '';
|
||||
} else if (account.getIn(['relationship', 'requested'])) {
|
||||
actionBtn = <Button className={classNames('logo-button')} text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.handleFollow} />;
|
||||
} else if (account.getIn(['relationship', 'muting'])) {
|
||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unmute)} onClick={this.handleMute} />;
|
||||
} else if (!account.getIn(['relationship', 'blocking'])) {
|
||||
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
|
||||
} else if (account.getIn(['relationship', 'blocking'])) {
|
||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
|
||||
}
|
||||
} else {
|
||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.handleEditProfile} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='directory__card'>
|
||||
<div className='directory__card__img'>
|
||||
<div className='account-card'>
|
||||
<Permalink href={account.get('url')} to={`/@${account.get('acct')}`} className='account-card__permalink'>
|
||||
<div className='account-card__header'>
|
||||
<img
|
||||
src={
|
||||
autoPlayGif ? account.get('header') : account.get('header_static')
|
||||
@ -209,36 +171,31 @@ class AccountCard extends ImmutablePureComponent {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='directory__card__bar'>
|
||||
<Permalink
|
||||
className='directory__card__bar__name'
|
||||
href={account.get('url')}
|
||||
to={`/@${account.get('acct')}`}
|
||||
>
|
||||
<Avatar account={account} size={48} />
|
||||
<div className='account-card__title'>
|
||||
<div className='account-card__title__avatar'><Avatar account={account} size={56} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
</Permalink>
|
||||
|
||||
<div className='directory__card__bar__relationship account__relationship'>
|
||||
{buttons}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='directory__card__extra' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||
{account.get('note').length > 0 && (
|
||||
<div
|
||||
className='account__header__content translate'
|
||||
className='account-card__bio translate'
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='directory__card__extra'>
|
||||
<div className='accounts-table__count'>
|
||||
<div className='account-card__actions'>
|
||||
<div className='account-card__counters'>
|
||||
<div className='account-card__counters__item'>
|
||||
<ShortNumber value={account.get('statuses_count')} />
|
||||
<small>
|
||||
<FormattedMessage id='account.posts' defaultMessage='Toots' />
|
||||
</small>
|
||||
</div>
|
||||
<div className='accounts-table__count'>
|
||||
|
||||
<div className='account-card__counters__item'>
|
||||
<ShortNumber value={account.get('followers_count')} />{' '}
|
||||
<small>
|
||||
<FormattedMessage
|
||||
@ -247,23 +204,22 @@ class AccountCard extends ImmutablePureComponent {
|
||||
/>
|
||||
</small>
|
||||
</div>
|
||||
<div className='accounts-table__count'>
|
||||
{account.get('last_status_at') === null ? (
|
||||
<FormattedMessage
|
||||
id='account.never_active'
|
||||
defaultMessage='Never'
|
||||
/>
|
||||
) : (
|
||||
<RelativeTimestamp timestamp={account.get('last_status_at')} />
|
||||
)}{' '}
|
||||
|
||||
<div className='account-card__counters__item'>
|
||||
<ShortNumber value={account.get('following_count')} />{' '}
|
||||
<small>
|
||||
<FormattedMessage
|
||||
id='account.last_status'
|
||||
defaultMessage='Last active'
|
||||
id='account.following'
|
||||
defaultMessage='Following'
|
||||
/>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='account-card__actions__button'>
|
||||
{actionBtn}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -10,9 +10,9 @@ import { fetchDirectory, expandDirectory } from 'mastodon/actions/directory';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import AccountCard from './components/account_card';
|
||||
import RadioButton from 'mastodon/components/radio_button';
|
||||
import classNames from 'classnames';
|
||||
import LoadMore from 'mastodon/components/load_more';
|
||||
import ScrollContainer from 'mastodon/containers/scroll_container';
|
||||
import LoadingIndicator from 'mastodon/components/loading_indicator';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
|
||||
@ -129,7 +129,7 @@ class Directory extends React.PureComponent {
|
||||
const pinned = !!columnId;
|
||||
|
||||
const scrollableArea = (
|
||||
<div className='scrollable' style={{ background: 'transparent' }}>
|
||||
<div className='scrollable'>
|
||||
<div className='filter-form'>
|
||||
<div className='filter-form__column' role='group'>
|
||||
<RadioButton name='order' value='active' label={intl.formatMessage(messages.recentlyActive)} checked={order === 'active'} onChange={this.handleChangeOrder} />
|
||||
@ -142,8 +142,10 @@ class Directory extends React.PureComponent {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={classNames('directory__list', { loading: isLoading })}>
|
||||
{accountIds.map(accountId => <AccountCard id={accountId} key={accountId} />)}
|
||||
<div className='directory__list'>
|
||||
{isLoading ? <LoadingIndicator /> : accountIds.map(accountId => (
|
||||
<AccountCard id={accountId} key={accountId} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<LoadMore onClick={this.handleLoadMore} visible={!isLoading} />
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Account from 'mastodon/containers/account_container';
|
||||
import AccountCard from 'mastodon/features/directory/components/account_card';
|
||||
import LoadingIndicator from 'mastodon/components/loading_indicator';
|
||||
import { connect } from 'react-redux';
|
||||
import { fetchSuggestions } from 'mastodon/actions/suggestions';
|
||||
@ -29,9 +29,9 @@ class Suggestions extends React.PureComponent {
|
||||
const { isLoading, suggestions } = this.props;
|
||||
|
||||
return (
|
||||
<div className='explore__links'>
|
||||
{isLoading ? (<LoadingIndicator />) : suggestions.map(suggestion => (
|
||||
<Account key={suggestion.get('account')} id={suggestion.get('account')} />
|
||||
<div className='explore__suggestions'>
|
||||
{isLoading ? <LoadingIndicator /> : suggestions.map(suggestion => (
|
||||
<AccountCard key={suggestion.get('account')} id={suggestion.get('account')} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
@ -8,7 +8,7 @@ const messages = defineMessages({
|
||||
dislike: { id: 'report.reasons.dislike', defaultMessage: 'I don\'t like it' },
|
||||
dislike_description: { id: 'report.reasons.dislike_description', defaultMessage: 'It is not something you want to see' },
|
||||
spam: { id: 'report.reasons.spam', defaultMessage: 'It\'s spam' },
|
||||
spam_description: { id: 'report.reasons.spam_description', defaultMessage: 'Malicious links, fake engagement, or repetetive replies' },
|
||||
spam_description: { id: 'report.reasons.spam_description', defaultMessage: 'Malicious links, fake engagement, or repetitive replies' },
|
||||
violation: { id: 'report.reasons.violation', defaultMessage: 'It violates server rules' },
|
||||
violation_description: { id: 'report.reasons.violation_description', defaultMessage: 'You are aware that it breaks specific rules' },
|
||||
other: { id: 'report.reasons.other', defaultMessage: 'It\'s something else' },
|
||||
|
@ -121,7 +121,7 @@ class Video extends React.PureComponent {
|
||||
autoPlay: PropTypes.bool,
|
||||
volume: PropTypes.number,
|
||||
muted: PropTypes.bool,
|
||||
componetIndex: PropTypes.number,
|
||||
componentIndex: PropTypes.number,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -502,7 +502,7 @@ class Video extends React.PureComponent {
|
||||
startTime: this.video.currentTime,
|
||||
autoPlay: !this.state.paused,
|
||||
defaultVolume: this.state.volume,
|
||||
componetIndex: this.props.componetIndex,
|
||||
componentIndex: this.props.componentIndex,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -414,7 +414,7 @@
|
||||
"report.reasons.other": "It's something else",
|
||||
"report.reasons.other_description": "The issue does not fit into other categories",
|
||||
"report.reasons.spam": "It's spam",
|
||||
"report.reasons.spam_description": "Malicious links, fake engagement, or repetetive replies",
|
||||
"report.reasons.spam_description": "Malicious links, fake engagement, or repetitive replies",
|
||||
"report.reasons.violation": "It violates server rules",
|
||||
"report.reasons.violation_description": "You are aware that it breaks specific rules",
|
||||
"report.rules.subtitle": "Select all that apply",
|
||||
|
@ -40,19 +40,11 @@ html {
|
||||
background: lighten($ui-base-color, 12%);
|
||||
}
|
||||
|
||||
.filter-form,
|
||||
.directory__card__bar {
|
||||
.filter-form {
|
||||
background: $white;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
}
|
||||
|
||||
.scrollable .directory__list {
|
||||
width: calc(100% + 2px);
|
||||
margin-left: -1px;
|
||||
margin-right: -1px;
|
||||
}
|
||||
|
||||
.directory__card,
|
||||
.table-of-contents {
|
||||
border: 1px solid lighten($ui-base-color, 8%);
|
||||
}
|
||||
@ -75,8 +67,7 @@ html {
|
||||
.column-header__back-button,
|
||||
.column-header__button,
|
||||
.column-header__button.active,
|
||||
.account__header__bar,
|
||||
.directory__card__extra {
|
||||
.account__header__bar {
|
||||
background: $white;
|
||||
}
|
||||
|
||||
|
@ -1236,6 +1236,11 @@ a.sparkline {
|
||||
background: $ui-base-color;
|
||||
border-radius: 4px;
|
||||
|
||||
&__permalink {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&__header {
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
@ -1252,20 +1257,22 @@ a.sparkline {
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin-top: -25px;
|
||||
margin-top: -(15px + 8px);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
&__avatar {
|
||||
padding: 15px;
|
||||
padding: 14px;
|
||||
|
||||
img {
|
||||
img,
|
||||
.account__avatar {
|
||||
display: block;
|
||||
margin: 0;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: darken($ui-base-color, 8%);
|
||||
background-color: darken($ui-base-color, 8%);
|
||||
border-radius: 8px;
|
||||
border: 1px solid $ui-base-color;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1273,30 +1280,34 @@ a.sparkline {
|
||||
color: $darker-text-color;
|
||||
padding-bottom: 15px;
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
|
||||
bdi {
|
||||
display: block;
|
||||
color: $primary-text-color;
|
||||
font-weight: 500;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__bio {
|
||||
padding: 0 15px;
|
||||
margin: 8px 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-wrap: break-word;
|
||||
max-height: 18px * 2;
|
||||
max-height: 21px * 2;
|
||||
position: relative;
|
||||
font-size: 15px;
|
||||
line-height: 21px;
|
||||
|
||||
&::after {
|
||||
display: block;
|
||||
content: "";
|
||||
width: 50px;
|
||||
height: 18px;
|
||||
height: 21px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
bottom: 8px;
|
||||
right: 15px;
|
||||
background: linear-gradient(to left, $ui-base-color, transparent);
|
||||
pointer-events: none;
|
||||
@ -1309,10 +1320,6 @@ a.sparkline {
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
|
||||
.fa {
|
||||
color: lighten($dark-text-color, 7%);
|
||||
}
|
||||
}
|
||||
|
||||
&.mention {
|
||||
@ -1329,12 +1336,21 @@ a.sparkline {
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 10px;
|
||||
|
||||
&__button {
|
||||
flex: 0 0 auto;
|
||||
flex-shrink: 1;
|
||||
padding: 0 15px;
|
||||
overflow: hidden;
|
||||
|
||||
.button {
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1343,19 +1359,23 @@ a.sparkline {
|
||||
display: grid;
|
||||
grid-auto-columns: minmax(0, 1fr);
|
||||
grid-auto-flow: column;
|
||||
max-width: 340px;
|
||||
min-width: 65px * 3;
|
||||
|
||||
&__item {
|
||||
padding: 15px;
|
||||
padding: 15px 0;
|
||||
text-align: center;
|
||||
color: $primary-text-color;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
line-height: 21px;
|
||||
|
||||
small {
|
||||
display: block;
|
||||
color: $darker-text-color;
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -50,7 +50,7 @@
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-family: inherit;
|
||||
font-size: 17px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0;
|
||||
line-height: 22px;
|
||||
@ -2333,17 +2333,7 @@ a.account__display-name {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.directory__list {
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
grid-template-columns: minmax(0, 50%) minmax(0, 50%);
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.directory__card {
|
||||
.account-card {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@ -4315,7 +4305,7 @@ a.status-card.compact:hover {
|
||||
}
|
||||
}
|
||||
|
||||
.upload-progess__message {
|
||||
.upload-progress__message {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
@ -6219,136 +6209,20 @@ a.status-card.compact:hover {
|
||||
}
|
||||
}
|
||||
|
||||
.directory {
|
||||
&__list {
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
transition: opacity 100ms ease-in;
|
||||
|
||||
&.loading {
|
||||
opacity: 0.7;
|
||||
.scrollable .account-card {
|
||||
margin: 10px;
|
||||
background: lighten($ui-base-color, 8%);
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
margin: 0;
|
||||
.scrollable .account-card__title__avatar {
|
||||
img,
|
||||
.account__avatar {
|
||||
border-color: lighten($ui-base-color, 8%);
|
||||
}
|
||||
}
|
||||
|
||||
&__card {
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&__img {
|
||||
height: 125px;
|
||||
position: relative;
|
||||
background: darken($ui-base-color, 12%);
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
&__bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: lighten($ui-base-color, 4%);
|
||||
padding: 10px;
|
||||
|
||||
&__name {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__relationship {
|
||||
width: 23px;
|
||||
min-height: 1px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
flex: 0 0 auto;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
padding-top: 2px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
margin: 0;
|
||||
border-radius: 4px;
|
||||
background: darken($ui-base-color, 8%);
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.display-name {
|
||||
margin-left: 15px;
|
||||
text-align: left;
|
||||
|
||||
strong {
|
||||
font-size: 15px;
|
||||
color: $primary-text-color;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
span {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: $darker-text-color;
|
||||
font-weight: 400;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__extra {
|
||||
background: $ui-base-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.accounts-table__count {
|
||||
width: 33.33%;
|
||||
flex: 0 0 auto;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.account__header__content {
|
||||
box-sizing: border-box;
|
||||
padding: 15px 10px;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
width: 100%;
|
||||
min-height: 18px + 30px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
p {
|
||||
display: none;
|
||||
|
||||
&:first-child {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
br {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.scrollable .account-card__bio::after {
|
||||
background: linear-gradient(to left, lighten($ui-base-color, 8%), transparent);
|
||||
}
|
||||
|
||||
.account-gallery__container {
|
||||
@ -6452,6 +6326,7 @@ a.status-card.compact:hover {
|
||||
|
||||
&__column {
|
||||
padding: 10px 15px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.radio-button {
|
||||
|
@ -409,14 +409,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.directory__card {
|
||||
border-radius: 4px;
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.page-header {
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
border-bottom: 0;
|
||||
@ -835,17 +827,19 @@
|
||||
grid-gap: 10px;
|
||||
grid-template-columns: minmax(0, 50%) minmax(0, 50%);
|
||||
|
||||
.account-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $no-gap-breakpoint) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
font-size: 18px;
|
||||
.account-card {
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.directory__card {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
|
@ -69,7 +69,7 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.autossugest-input {
|
||||
.autosuggest-input {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
|
@ -12,11 +12,6 @@ body.rtl {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.directory__card__bar .display-name {
|
||||
margin-left: 0;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.display-name,
|
||||
.announcements__item {
|
||||
text-align: right;
|
||||
|
@ -32,7 +32,6 @@ class UserSettingsDecorator
|
||||
user.settings['system_font_ui'] = system_font_ui_preference if change?('setting_system_font_ui')
|
||||
user.settings['system_emoji_font'] = system_emoji_font_preference if change?('setting_system_emoji_font')
|
||||
user.settings['noindex'] = noindex_preference if change?('setting_noindex')
|
||||
user.settings['hide_followers_count']= hide_followers_count_preference if change?('setting_hide_followers_count')
|
||||
user.settings['flavour'] = flavour_preference if change?('setting_flavour')
|
||||
user.settings['skin'] = skin_preference if change?('setting_skin')
|
||||
user.settings['hide_network'] = hide_network_preference if change?('setting_hide_network')
|
||||
@ -110,10 +109,6 @@ class UserSettingsDecorator
|
||||
boolean_cast_setting 'setting_noindex'
|
||||
end
|
||||
|
||||
def hide_followers_count_preference
|
||||
boolean_cast_setting 'setting_hide_followers_count'
|
||||
end
|
||||
|
||||
def flavour_preference
|
||||
settings['setting_flavour']
|
||||
end
|
||||
|
@ -351,11 +351,11 @@ class Account < ApplicationRecord
|
||||
end
|
||||
|
||||
def hides_followers?
|
||||
hide_collections? || user_hides_network?
|
||||
hide_collections?
|
||||
end
|
||||
|
||||
def hides_following?
|
||||
hide_collections? || user_hides_network?
|
||||
hide_collections?
|
||||
end
|
||||
|
||||
def object_type
|
||||
|
134
app/models/account_statuses_filter.rb
Normal file
134
app/models/account_statuses_filter.rb
Normal file
@ -0,0 +1,134 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AccountStatusesFilter
|
||||
KEYS = %i(
|
||||
pinned
|
||||
tagged
|
||||
only_media
|
||||
exclude_replies
|
||||
exclude_reblogs
|
||||
).freeze
|
||||
|
||||
attr_reader :params, :account, :current_account
|
||||
|
||||
def initialize(account, current_account, params = {})
|
||||
@account = account
|
||||
@current_account = current_account
|
||||
@params = params
|
||||
end
|
||||
|
||||
def results
|
||||
scope = initial_scope
|
||||
|
||||
scope.merge!(pinned_scope) if pinned?
|
||||
scope.merge!(only_media_scope) if only_media?
|
||||
scope.merge!(no_replies_scope) if exclude_replies?
|
||||
scope.merge!(no_reblogs_scope) if exclude_reblogs?
|
||||
scope.merge!(hashtag_scope) if tagged?
|
||||
|
||||
scope
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def initial_scope
|
||||
if suspended?
|
||||
Status.none
|
||||
elsif anonymous?
|
||||
account.statuses.not_local_only.where(visibility: %i(public unlisted))
|
||||
elsif author?
|
||||
account.statuses.all # NOTE: #merge! does not work without the #all
|
||||
elsif blocked?
|
||||
Status.none
|
||||
else
|
||||
filtered_scope
|
||||
end
|
||||
end
|
||||
|
||||
def filtered_scope
|
||||
scope = account.statuses.left_outer_joins(:mentions)
|
||||
|
||||
scope.merge!(scope.where(visibility: follower? ? %i(public unlisted private) : %i(public unlisted)).or(scope.where(mentions: { account_id: current_account.id })).group(Status.arel_table[:id]))
|
||||
scope.merge!(filtered_reblogs_scope) if reblogs_may_occur?
|
||||
|
||||
scope
|
||||
end
|
||||
|
||||
def filtered_reblogs_scope
|
||||
Status.left_outer_joins(:reblog).where(reblog_of_id: nil).or(Status.where.not(reblogs_statuses: { account_id: current_account.excluded_from_timeline_account_ids }))
|
||||
end
|
||||
|
||||
def only_media_scope
|
||||
Status.joins(:media_attachments).merge(account.media_attachments.reorder(nil)).group(Status.arel_table[:id])
|
||||
end
|
||||
|
||||
def no_replies_scope
|
||||
Status.without_replies
|
||||
end
|
||||
|
||||
def no_reblogs_scope
|
||||
Status.without_reblogs
|
||||
end
|
||||
|
||||
def pinned_scope
|
||||
account.pinned_statuses.group(Status.arel_table[:id], StatusPin.arel_table[:created_at])
|
||||
end
|
||||
|
||||
def hashtag_scope
|
||||
tag = Tag.find_normalized(params[:tagged])
|
||||
|
||||
if tag
|
||||
Status.tagged_with(tag.id)
|
||||
else
|
||||
Status.none
|
||||
end
|
||||
end
|
||||
|
||||
def suspended?
|
||||
account.suspended?
|
||||
end
|
||||
|
||||
def anonymous?
|
||||
current_account.nil?
|
||||
end
|
||||
|
||||
def author?
|
||||
current_account.id == account.id
|
||||
end
|
||||
|
||||
def blocked?
|
||||
account.blocking?(current_account) || (current_account.domain.present? && account.domain_blocking?(current_account.domain))
|
||||
end
|
||||
|
||||
def follower?
|
||||
current_account.following?(account)
|
||||
end
|
||||
|
||||
def reblogs_may_occur?
|
||||
!exclude_reblogs? && !only_media? && !tagged?
|
||||
end
|
||||
|
||||
def pinned?
|
||||
truthy_param?(:pinned)
|
||||
end
|
||||
|
||||
def only_media?
|
||||
truthy_param?(:only_media)
|
||||
end
|
||||
|
||||
def exclude_replies?
|
||||
truthy_param?(:exclude_replies)
|
||||
end
|
||||
|
||||
def exclude_reblogs?
|
||||
truthy_param?(:exclude_reblogs)
|
||||
end
|
||||
|
||||
def tagged?
|
||||
params[:tagged].present?
|
||||
end
|
||||
|
||||
def truthy_param?(key)
|
||||
ActiveModel::Type::Boolean.new.cast(params[key])
|
||||
end
|
||||
end
|
@ -129,6 +129,6 @@ class Report < ApplicationRecord
|
||||
def validate_rule_ids
|
||||
return unless violation?
|
||||
|
||||
errors.add(:rule_ids, I18n.t('reports.errors.invalid_rules')) unless rules.size == rule_ids.size
|
||||
errors.add(:rule_ids, I18n.t('reports.errors.invalid_rules')) unless rules.size == rule_ids&.size
|
||||
end
|
||||
end
|
||||
|
@ -399,28 +399,6 @@ class Status < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def permitted_for(target_account, account)
|
||||
visibility = [:public, :unlisted]
|
||||
|
||||
if account.nil?
|
||||
where(visibility: visibility).not_local_only
|
||||
elsif target_account.blocking?(account) || (account.domain.present? && target_account.domain_blocking?(account.domain)) # get rid of blocked peeps
|
||||
none
|
||||
elsif account.id == target_account.id # author can see own stuff
|
||||
all
|
||||
else
|
||||
# followers can see followers-only stuff, but also things they are mentioned in.
|
||||
# non-followers can see everything that isn't private/direct, but can see stuff they are mentioned in.
|
||||
visibility.push(:private) if account.following?(target_account)
|
||||
|
||||
scope = left_outer_joins(:reblog)
|
||||
|
||||
scope.where(visibility: visibility)
|
||||
.or(scope.where(id: account.mentions.select(:status_id)))
|
||||
.merge(scope.where(reblog_of_id: nil).or(scope.where.not(reblogs_statuses: { account_id: account.excluded_from_timeline_account_ids })))
|
||||
end
|
||||
end
|
||||
|
||||
def from_text(text)
|
||||
return [] if text.blank?
|
||||
|
||||
|
@ -126,7 +126,7 @@ class User < ApplicationRecord
|
||||
has_many :session_activations, dependent: :destroy
|
||||
|
||||
delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :favourite_modal, :delete_modal,
|
||||
:reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_media, :hide_network, :hide_followers_count,
|
||||
:reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_media, :hide_followers_count,
|
||||
:expand_spoilers, :default_language, :aggregate_reblogs, :show_application,
|
||||
:advanced_layout, :use_blurhash, :use_pending_items, :trends, :crop_images,
|
||||
:disable_swiping, :default_content_type, :system_emoji_font,
|
||||
@ -281,10 +281,6 @@ class User < ApplicationRecord
|
||||
settings.notification_emails['trending_status']
|
||||
end
|
||||
|
||||
def hides_network?
|
||||
@hides_network ||= settings.hide_network
|
||||
end
|
||||
|
||||
def aggregates_reblogs?
|
||||
@aggregates_reblogs ||= settings.aggregate_reblogs
|
||||
end
|
||||
|
@ -42,7 +42,7 @@ class UserPolicy < ApplicationPolicy
|
||||
end
|
||||
|
||||
def promote?
|
||||
admin? && promoteable?
|
||||
admin? && promotable?
|
||||
end
|
||||
|
||||
def demote?
|
||||
@ -51,7 +51,7 @@ class UserPolicy < ApplicationPolicy
|
||||
|
||||
private
|
||||
|
||||
def promoteable?
|
||||
def promotable?
|
||||
record.approved? && (!record.staff? || !record.admin?)
|
||||
end
|
||||
|
||||
|
17
app/presenters/familiar_followers_presenter.rb
Normal file
17
app/presenters/familiar_followers_presenter.rb
Normal file
@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class FamiliarFollowersPresenter
|
||||
class Result < ActiveModelSerializers::Model
|
||||
attributes :id, :accounts
|
||||
end
|
||||
|
||||
def initialize(accounts, current_account_id)
|
||||
@accounts = accounts
|
||||
@current_account_id = current_account_id
|
||||
end
|
||||
|
||||
def accounts
|
||||
map = Follow.includes(account: :account_stat).where(target_account_id: @accounts.map(&:id)).where(account_id: Follow.where(account_id: @current_account_id).joins(:target_account).merge(Account.where(hide_collections: [nil, false])).select(:target_account_id)).group_by(&:target_account_id)
|
||||
@accounts.map { |account| Result.new(id: account.id, accounts: (account.hide_collections? ? [] : (map[account.id] || [])).map(&:account)) }
|
||||
end
|
||||
end
|
11
app/serializers/rest/familiar_followers_serializer.rb
Normal file
11
app/serializers/rest/familiar_followers_serializer.rb
Normal file
@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class REST::FamiliarFollowersSerializer < ActiveModel::Serializer
|
||||
attribute :id
|
||||
|
||||
has_many :accounts, serializer: REST::AccountSerializer
|
||||
|
||||
def id
|
||||
object.id.to_s
|
||||
end
|
||||
end
|
@ -10,8 +10,8 @@
|
||||
= account_fields.input :username, wrapper: :with_label, label: false, required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off', placeholder: t('simple_form.labels.defaults.username'), pattern: '[a-zA-Z0-9_]+', maxlength: 30 }, append: "@#{site_hostname}", hint: false, disabled: disabled
|
||||
|
||||
= f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' }, hint: false, disabled: disabled
|
||||
= f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off', :minlength => User.password_length.first, :maxlength => User.password_length.last }, hint: false, disabled: disabled
|
||||
= f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }, hint: false, disabled: disabled
|
||||
= f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'new-password', :minlength => User.password_length.first, :maxlength => User.password_length.last }, hint: false, disabled: disabled
|
||||
= f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'new-password' }, hint: false, disabled: disabled
|
||||
|
||||
= f.input :confirm_password, as: :string, placeholder: t('simple_form.labels.defaults.honeypot', label: t('simple_form.labels.defaults.password')), required: false, input_html: { 'aria-label' => t('simple_form.labels.defaults.honeypot', label: t('simple_form.labels.defaults.password')), :autocomplete => 'off' }, hint: false, disabled: disabled
|
||||
= f.input :website, as: :url, placeholder: t('simple_form.labels.defaults.honeypot', label: 'Website'), required: false, input_html: { 'aria-label' => t('simple_form.labels.defaults.honeypot', label: 'Website'), :autocomplete => 'off' }, hint: false, disabled: disabled
|
||||
|
@ -19,37 +19,36 @@
|
||||
- else
|
||||
.directory__list
|
||||
- @accounts.each do |account|
|
||||
.directory__card
|
||||
.directory__card__img
|
||||
.account-card
|
||||
= link_to TagManager.instance.url_for(account), class: 'account-card__permalink' do
|
||||
.account-card__header
|
||||
= image_tag account.header.url, alt: ''
|
||||
.directory__card__bar
|
||||
= link_to TagManager.instance.url_for(account), class: 'directory__card__bar__name' do
|
||||
.avatar
|
||||
= image_tag account.avatar.url, alt: '', class: 'u-photo'
|
||||
|
||||
.account-card__title
|
||||
.account-card__title__avatar
|
||||
= image_tag account.avatar.url, alt: ''
|
||||
.display-name
|
||||
%bdi
|
||||
%strong.emojify.p-name= display_name(account, custom_emojify: true)
|
||||
%span= acct(account)
|
||||
.directory__card__bar__relationship.account__relationship
|
||||
= minimal_account_action_button(account)
|
||||
|
||||
.directory__card__extra
|
||||
.account__header__content.emojify= Formatter.instance.simplified_format(account, custom_emojify: true)
|
||||
|
||||
.directory__card__extra
|
||||
.accounts-table__count
|
||||
%span
|
||||
= acct(account)
|
||||
= fa_icon('lock') if account.locked?
|
||||
- if account.note.present?
|
||||
.account-card__bio.emojify
|
||||
= Formatter.instance.simplified_format(account, custom_emojify: true)
|
||||
- else
|
||||
.flex-spacer
|
||||
.account-card__actions
|
||||
.account-card__counters
|
||||
.account-card__counters__item
|
||||
= friendly_number_to_human account.statuses_count
|
||||
%small= t('accounts.posts', count: account.statuses_count).downcase
|
||||
.accounts-table__count
|
||||
.account-card__counters__item
|
||||
= hide_followers_count?(account) ? '-' : (friendly_number_to_human account.followers_count)
|
||||
%small= t('accounts.followers', count: account.followers_count).downcase
|
||||
.accounts-table__count
|
||||
- if account.last_status_at.present?
|
||||
%time.time-ago{ datetime: account.last_status_at.to_date.iso8601, title: l(account.last_status_at.to_date) }= l account.last_status_at.to_date
|
||||
- else
|
||||
= t('accounts.never_active')
|
||||
|
||||
%small= t('accounts.last_active')
|
||||
.account-card__counters__item
|
||||
= friendly_number_to_human account.following_count
|
||||
%small= t('accounts.following', count: account.following_count).downcase
|
||||
.account-card__actions__button
|
||||
= account_action_button(account)
|
||||
|
||||
= paginate @accounts
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
= render 'accounts/header', account: @account
|
||||
|
||||
- if @account.user_hides_network?
|
||||
- if @account.hide_collections?
|
||||
.nothing-here= t('accounts.network_hidden')
|
||||
- elsif user_signed_in? && @account.blocking?(current_account)
|
||||
.nothing-here= t('accounts.unavailable')
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
= render 'accounts/header', account: @account
|
||||
|
||||
- if @account.user_hides_network?
|
||||
- if @account.hide_collections?
|
||||
.nothing-here= t('accounts.network_hidden')
|
||||
- elsif user_signed_in? && @account.blocking?(current_account)
|
||||
.nothing-here= t('accounts.unavailable')
|
||||
|
@ -10,9 +10,6 @@
|
||||
.fields-group
|
||||
= f.input :setting_noindex, as: :boolean, wrapper: :with_label
|
||||
|
||||
.fields-group
|
||||
= f.input :setting_hide_network, as: :boolean, wrapper: :with_label
|
||||
|
||||
.fields-group
|
||||
= f.input :setting_aggregate_reblogs, as: :boolean, wrapper: :with_label, recommended: true
|
||||
|
||||
|
@ -30,7 +30,10 @@
|
||||
= f.input :bot, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.bot')
|
||||
|
||||
.fields-group
|
||||
= f.input :discoverable, as: :boolean, wrapper: :with_label, hint: t(Setting.profile_directory ? 'simple_form.hints.defaults.discoverable' : 'simple_form.hints.defaults.discoverable_no_directory'), recommended: true
|
||||
= f.input :discoverable, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.discoverable'), recommended: true
|
||||
|
||||
.fields-group
|
||||
= f.input :hide_collections, as: :boolean, wrapper: :with_label, label: t('simple_form.labels.defaults.setting_hide_network'), hint: t('simple_form.hints.defaults.setting_hide_network')
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
Rails.application.config.middleware.use OmniAuth::Builder do
|
||||
# Vanilla omniauth stategies
|
||||
# Vanilla omniauth strategies
|
||||
end
|
||||
|
||||
Devise.setup do |config|
|
||||
|
@ -72,7 +72,6 @@ en:
|
||||
media: Media
|
||||
moved_html: "%{name} has moved to %{new_profile_link}:"
|
||||
network_hidden: This information is not available
|
||||
never_active: Never
|
||||
nothing_here: There is nothing here!
|
||||
people_followed_by: People whom %{name} follows
|
||||
people_who_follow: People who follow %{name}
|
||||
|
@ -986,7 +986,7 @@ en_GB:
|
||||
enabled: Two-factor authentication is enabled
|
||||
enabled_success: Two-factor authentication successfully enabled
|
||||
generate_recovery_codes: Generate recovery codes
|
||||
instructions_html: "<strong>Scan this QR code into Google Authenticator or a similiar TOTP app on your phone</strong>. From now on, that app will generate tokens that you will have to enter when logging in."
|
||||
instructions_html: "<strong>Scan this QR code into Google Authenticator or a similar TOTP app on your phone</strong>. From now on, that app will generate tokens that you will have to enter when logging in."
|
||||
lost_recovery_codes: Recovery codes allow you to regain access to your account if you lose your phone. If you've lost your recovery codes, you can regenerate them here. Your old recovery codes will be invalidated.
|
||||
manual_instructions: 'If you can''t scan the QR code and need to enter it manually, here is the plain-text secret:'
|
||||
recovery_codes: Backup recovery codes
|
||||
|
@ -37,8 +37,7 @@ en:
|
||||
current_password: For security purposes please enter the password of the current account
|
||||
current_username: To confirm, please enter the username of the current account
|
||||
digest: Only sent after a long period of inactivity and only if you have received any personal messages in your absence
|
||||
discoverable: Allow your account to be discovered by strangers through recommendations, profile directory and other features
|
||||
discoverable_no_directory: Allow your account to be discovered by strangers through recommendations and other features
|
||||
discoverable: Allow your account to be discovered by strangers through recommendations, trends and other features
|
||||
email: You will be sent a confirmation e-mail
|
||||
fields: You can have up to 4 items displayed as a table on your profile
|
||||
header: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px
|
||||
|
@ -498,6 +498,7 @@ Rails.application.routes.draw do
|
||||
resource :search, only: :show, controller: :search
|
||||
resource :lookup, only: :show, controller: :lookup
|
||||
resources :relationships, only: :index
|
||||
resources :familiar_followers, only: :index
|
||||
end
|
||||
|
||||
resources :accounts, only: [:create, :show] do
|
||||
|
@ -17,7 +17,6 @@ defaults: &defaults
|
||||
min_invite_role: 'admin'
|
||||
show_staff_badge: true
|
||||
default_sensitive: false
|
||||
hide_network: false
|
||||
unfollow_modal: false
|
||||
boost_modal: false
|
||||
favourite_modal: false
|
||||
|
@ -16,7 +16,7 @@ class FixReblogsInFeeds < ActiveRecord::Migration[5.1]
|
||||
# is once again set to the reblogging status' ID, and the value
|
||||
# is set to the reblogged status' ID). This is safe for Redis'
|
||||
# float conversion because in this reblog tracking zset, we only
|
||||
# need the rebloggging status' ID to be able to stop tracking
|
||||
# need the reblogging status' ID to be able to stop tracking
|
||||
# entries after they have gotten too far down the feed, which
|
||||
# does not require an exact value.
|
||||
|
||||
|
@ -22,13 +22,13 @@ class RejectFollowingBlockedUsers < ActiveRecord::Migration[5.2]
|
||||
|
||||
follows.each do |follow|
|
||||
blocked_account = follow.account
|
||||
followed_acccount = follow.target_account
|
||||
followed_account = follow.target_account
|
||||
|
||||
next follow.destroy! if blocked_account.local?
|
||||
|
||||
reject_follow_json = Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(follow, serializer: ActivityPub::RejectFollowSerializer, adapter: ActivityPub::Adapter).as_json).sign!(followed_acccount))
|
||||
reject_follow_json = Oj.dump(ActivityPub::LinkedDataSignature.new(ActiveModelSerializers::SerializableResource.new(follow, serializer: ActivityPub::RejectFollowSerializer, adapter: ActivityPub::Adapter).as_json).sign!(followed_account))
|
||||
|
||||
ActivityPub::DeliveryWorker.perform_async(reject_follow_json, followed_acccount, blocked_account.inbox_url)
|
||||
ActivityPub::DeliveryWorker.perform_async(reject_follow_json, followed_account, blocked_account.inbox_url)
|
||||
|
||||
follow.destroy!
|
||||
end
|
||||
|
37
db/migrate/20220304195405_migrate_hide_network_preference.rb
Normal file
37
db/migrate/20220304195405_migrate_hide_network_preference.rb
Normal file
@ -0,0 +1,37 @@
|
||||
class MigrateHideNetworkPreference < ActiveRecord::Migration[6.1]
|
||||
disable_ddl_transaction!
|
||||
|
||||
# Dummy classes, to make migration possible across version changes
|
||||
class Account < ApplicationRecord
|
||||
has_one :user, inverse_of: :account
|
||||
scope :local, -> { where(domain: nil) }
|
||||
end
|
||||
|
||||
class User < ApplicationRecord
|
||||
belongs_to :account
|
||||
end
|
||||
|
||||
def up
|
||||
Account.reset_column_information
|
||||
|
||||
Setting.unscoped.where(thing_type: 'User', var: 'hide_network').find_each do |setting|
|
||||
account = User.find(setting.thing_id).account
|
||||
|
||||
ApplicationRecord.transaction do
|
||||
account.update(hide_collections: setting.value)
|
||||
setting.delete
|
||||
end
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
Account.local.where(hide_collections: true).includes(:user).find_each do |account|
|
||||
ApplicationRecord.transaction do
|
||||
Setting.create(thing_type: 'User', thing_id: account.user.id, var: 'hide_network', value: account.hide_collections?)
|
||||
account.update(hide_collections: nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2022_02_27_041951) do
|
||||
ActiveRecord::Schema.define(version: 2022_03_04_195405) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
@ -41,7 +41,7 @@ module Mastodon
|
||||
|
||||
Gem::Package::TarReader.new(Zlib::GzipReader.open(path)) do |tar|
|
||||
tar.each do |entry|
|
||||
next unless entry.file? && entry.full_name.end_with?('.png')
|
||||
next unless entry.file? && entry.full_name.end_with?('.png', '.gif')
|
||||
|
||||
filename = File.basename(entry.full_name, '.*')
|
||||
|
||||
|
@ -510,7 +510,7 @@ module Mastodon
|
||||
accounts = accounts.sort_by(&:id).reverse
|
||||
|
||||
@prompt.warn "Multiple local accounts were found for username '#{accounts.first.username}'."
|
||||
@prompt.warn 'All those accounts are distinct accounts but only the most recently-created one is fully-functionnal.'
|
||||
@prompt.warn 'All those accounts are distinct accounts but only the most recently-created one is fully-functional.'
|
||||
|
||||
accounts.each_with_index do |account, idx|
|
||||
@prompt.say '%2d. %s: created at: %s; updated at: %s; last logged in at: %s; statuses: %5d; last status at: %s' % [idx, account.username, account.created_at, account.updated_at, account.user&.last_sign_in_at&.to_s || 'N/A', account.account_stat&.statuses_count || 0, account.account_stat&.last_status_at || 'N/A']
|
||||
|
@ -156,7 +156,7 @@ module Mastodon
|
||||
|
||||
ActiveRecord::Base.connection.add_index(:statuses, :conversation_id, name: :index_statuses_conversation_id, algorithm: :concurrently, if_not_exists: true)
|
||||
|
||||
say('Extract the deletion target from coversations... This might take a while...')
|
||||
say('Extract the deletion target from conversations... This might take a while...')
|
||||
|
||||
ActiveRecord::Base.connection.create_table('conversations_to_be_deleted', force: true)
|
||||
|
||||
|
@ -2,6 +2,50 @@
|
||||
|
||||
namespace :tests do
|
||||
namespace :migrations do
|
||||
desc 'Check that database state is consistent with a successful migration from populated data'
|
||||
task check_database: :environment do
|
||||
unless Account.find_by(username: 'admin', domain: nil)&.hide_collections? == false
|
||||
puts 'Unexpected value for Account#hide_collections? for user @admin'
|
||||
exit(1)
|
||||
end
|
||||
|
||||
unless Account.find_by(username: 'user', domain: nil)&.hide_collections? == true
|
||||
puts 'Unexpected value for Account#hide_collections? for user @user'
|
||||
exit(1)
|
||||
end
|
||||
|
||||
unless Account.find_by(username: 'evil', domain: 'activitypub.com')&.suspended?
|
||||
puts 'Unexpected value for Account#suspended? for user @evil@activitypub.com'
|
||||
exit(1)
|
||||
end
|
||||
|
||||
unless Status.find(6).account_id == Status.find(7).account_id
|
||||
puts 'Users @remote@remote.com and @Remote@remote.com not properly merged'
|
||||
exit(1)
|
||||
end
|
||||
|
||||
if Account.where(domain: Rails.configuration.x.local_domain).exists?
|
||||
puts 'Faux remote accounts not properly claned up'
|
||||
exit(1)
|
||||
end
|
||||
|
||||
unless AccountConversation.first&.last_status_id == 11
|
||||
puts 'AccountConversation records not created as expected'
|
||||
exit(1)
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Populate the database with test data for 2.4.0'
|
||||
task populate_v2_4: :environment do
|
||||
ActiveRecord::Base.connection.execute(<<~SQL)
|
||||
INSERT INTO "settings"
|
||||
(id, thing_type, thing_id, var, value, created_at, updated_at)
|
||||
VALUES
|
||||
(1, 'User', 1, 'hide_network', E'--- false\n', now(), now()),
|
||||
(2, 'User', 2, 'hide_network', E'--- true\n', now(), now());
|
||||
SQL
|
||||
end
|
||||
|
||||
desc 'Populate the database with test data for 2.0.0'
|
||||
task populate_v2: :environment do
|
||||
admin_key = OpenSSL::PKey::RSA.new(2048)
|
||||
@ -34,7 +78,7 @@ namespace :tests do
|
||||
'https://remote.com/@remote', 'https://remote.com/salmon/1'),
|
||||
(4, 'Remote', 'remote.com', NULL, #{remote_public_key}, now(), now(),
|
||||
'https://remote.com/@Remote', 'https://remote.com/salmon/1'),
|
||||
(5, 'REMOTE', 'Remote.com', NULL, #{remote_public_key2}, now(), now(),
|
||||
(5, 'REMOTE', 'Remote.com', NULL, #{remote_public_key2}, now() - interval '1 year', now() - interval '1 year',
|
||||
'https://remote.com/stale/@REMOTE', 'https://remote.com/stale/salmon/1');
|
||||
|
||||
INSERT INTO "accounts"
|
||||
@ -49,6 +93,13 @@ namespace :tests do
|
||||
(7, 'user', #{local_domain}, #{user_private_key}, #{user_public_key}, now(), now()),
|
||||
(8, 'pt_user', NULL, #{user_private_key}, #{user_public_key}, now(), now());
|
||||
|
||||
INSERT INTO "accounts"
|
||||
(id, username, domain, private_key, public_key, created_at, updated_at, protocol, inbox_url, outbox_url, followers_url, suspended)
|
||||
VALUES
|
||||
(9, 'evil', 'activitypub.com', NULL, #{remote_public_key_ap}, now(), now(),
|
||||
1, 'https://activitypub.com/users/evil/inbox', 'https://activitypub.com/users/evil/outbox',
|
||||
'https://activitypub.com/users/evil/followers', true);
|
||||
|
||||
-- users
|
||||
|
||||
INSERT INTO "users"
|
||||
@ -62,6 +113,9 @@ namespace :tests do
|
||||
VALUES
|
||||
(3, 7, 'ptuser@localhost', now(), now(), false, 'pt');
|
||||
|
||||
-- conversations
|
||||
INSERT INTO "conversations" (id, created_at, updated_at) VALUES (1, now(), now());
|
||||
|
||||
-- statuses
|
||||
|
||||
INSERT INTO "statuses"
|
||||
@ -97,14 +151,22 @@ namespace :tests do
|
||||
VALUES
|
||||
(9, 1, 2, now(), now());
|
||||
|
||||
INSERT INTO "statuses"
|
||||
(id, account_id, text, in_reply_to_id, conversation_id, visibility, created_at, updated_at)
|
||||
VALUES
|
||||
(10, 2, '@admin hey!', NULL, 1, 3, now(), now()),
|
||||
(11, 1, '@user hey!', 10, 1, 3, now(), now());
|
||||
|
||||
-- mentions (from previous statuses)
|
||||
|
||||
INSERT INTO "mentions"
|
||||
(status_id, account_id, created_at, updated_at)
|
||||
(id, status_id, account_id, created_at, updated_at)
|
||||
VALUES
|
||||
(2, 3, now(), now()),
|
||||
(3, 4, now(), now()),
|
||||
(4, 5, now(), now());
|
||||
(1, 2, 3, now(), now()),
|
||||
(2, 3, 4, now(), now()),
|
||||
(3, 4, 5, now(), now()),
|
||||
(4, 10, 1, now(), now()),
|
||||
(5, 11, 2, now(), now());
|
||||
|
||||
-- stream entries
|
||||
|
||||
@ -121,7 +183,6 @@ namespace :tests do
|
||||
(8, 5, 'status', now(), now()),
|
||||
(9, 1, 'status', now(), now());
|
||||
|
||||
|
||||
-- custom emoji
|
||||
|
||||
INSERT INTO "custom_emojis"
|
||||
@ -161,12 +222,12 @@ namespace :tests do
|
||||
-- follows
|
||||
|
||||
INSERT INTO "follows"
|
||||
(account_id, target_account_id, created_at, updated_at)
|
||||
(id, account_id, target_account_id, created_at, updated_at)
|
||||
VALUES
|
||||
(1, 5, now(), now()),
|
||||
(6, 2, now(), now()),
|
||||
(5, 2, now(), now()),
|
||||
(6, 1, now(), now());
|
||||
(1, 1, 5, now(), now()),
|
||||
(2, 6, 2, now(), now()),
|
||||
(3, 5, 2, now(), now()),
|
||||
(4, 6, 1, now(), now());
|
||||
|
||||
-- follow requests
|
||||
|
||||
@ -175,6 +236,15 @@ namespace :tests do
|
||||
VALUES
|
||||
(2, 5, now(), now()),
|
||||
(5, 1, now(), now());
|
||||
|
||||
-- notifications
|
||||
|
||||
INSERT INTO "notifications"
|
||||
(id, from_account_id, account_id, activity_type, activity_id, created_at, updated_at)
|
||||
VALUES
|
||||
(1, 6, 2, 'Follow', 2, now(), now()),
|
||||
(2, 2, 1, 'Mention', 4, now(), now()),
|
||||
(3, 1, 2, 'Mention', 5, now(), now());
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
@ -5,7 +5,7 @@ RSpec.describe AccountsController, type: :controller do
|
||||
|
||||
let(:account) { Fabricate(:account) }
|
||||
|
||||
shared_examples 'cachable response' do
|
||||
shared_examples 'cacheable response' do
|
||||
it 'does not set cookies' do
|
||||
expect(response.cookies).to be_empty
|
||||
expect(response.headers['Set-Cookies']).to be nil
|
||||
@ -374,7 +374,7 @@ RSpec.describe AccountsController, type: :controller do
|
||||
expect(response.media_type).to eq 'application/activity+json'
|
||||
end
|
||||
|
||||
it_behaves_like 'cachable response'
|
||||
it_behaves_like 'cacheable response'
|
||||
|
||||
it 'renders account' do
|
||||
json = body_as_json
|
||||
@ -432,7 +432,7 @@ RSpec.describe AccountsController, type: :controller do
|
||||
expect(response.media_type).to eq 'application/activity+json'
|
||||
end
|
||||
|
||||
it_behaves_like 'cachable response'
|
||||
it_behaves_like 'cacheable response'
|
||||
|
||||
it 'renders account' do
|
||||
json = body_as_json
|
||||
@ -499,7 +499,7 @@ RSpec.describe AccountsController, type: :controller do
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it_behaves_like 'cachable response'
|
||||
it_behaves_like 'cacheable response'
|
||||
end
|
||||
|
||||
context do
|
||||
|
@ -7,7 +7,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do
|
||||
let!(:private_pinned) { Fabricate(:status, account: account, text: 'secret private stuff', visibility: :private) }
|
||||
let(:remote_account) { nil }
|
||||
|
||||
shared_examples 'cachable response' do
|
||||
shared_examples 'cacheable response' do
|
||||
it 'does not set cookies' do
|
||||
expect(response.cookies).to be_empty
|
||||
expect(response.headers['Set-Cookies']).to be nil
|
||||
@ -48,7 +48,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do
|
||||
expect(response.media_type).to eq 'application/activity+json'
|
||||
end
|
||||
|
||||
it_behaves_like 'cachable response'
|
||||
it_behaves_like 'cacheable response'
|
||||
|
||||
it 'returns orderedItems with pinned statuses' do
|
||||
expect(body[:orderedItems]).to be_an Array
|
||||
@ -101,7 +101,7 @@ RSpec.describe ActivityPub::CollectionsController, type: :controller do
|
||||
expect(response.media_type).to eq 'application/activity+json'
|
||||
end
|
||||
|
||||
it_behaves_like 'cachable response'
|
||||
it_behaves_like 'cacheable response'
|
||||
|
||||
it 'returns orderedItems with pinned statuses' do
|
||||
json = body_as_json
|
||||
|
@ -3,7 +3,7 @@ require 'rails_helper'
|
||||
RSpec.describe ActivityPub::OutboxesController, type: :controller do
|
||||
let!(:account) { Fabricate(:account) }
|
||||
|
||||
shared_examples 'cachable response' do
|
||||
shared_examples 'cacheable response' do
|
||||
it 'does not set cookies' do
|
||||
expect(response.cookies).to be_empty
|
||||
expect(response.headers['Set-Cookies']).to be nil
|
||||
@ -53,7 +53,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
|
||||
expect(body[:totalItems]).to eq 4
|
||||
end
|
||||
|
||||
it_behaves_like 'cachable response'
|
||||
it_behaves_like 'cacheable response'
|
||||
|
||||
it 'does not have a Vary header' do
|
||||
expect(response.headers['Vary']).to be_nil
|
||||
@ -98,7 +98,7 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do
|
||||
expect(body[:orderedItems].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true
|
||||
end
|
||||
|
||||
it_behaves_like 'cachable response'
|
||||
it_behaves_like 'cacheable response'
|
||||
|
||||
it 'returns Vary header with Signature' do
|
||||
expect(response.headers['Vary']).to include 'Signature'
|
||||
|
@ -8,7 +8,7 @@ RSpec.describe ActivityPub::RepliesController, type: :controller do
|
||||
let(:remote_reply_id) { 'https://foobar.com/statuses/1234' }
|
||||
let(:remote_querier) { nil }
|
||||
|
||||
shared_examples 'cachable response' do
|
||||
shared_examples 'cacheable response' do
|
||||
it 'does not set cookies' do
|
||||
expect(response.cookies).to be_empty
|
||||
expect(response.headers['Set-Cookies']).to be nil
|
||||
@ -93,7 +93,7 @@ RSpec.describe ActivityPub::RepliesController, type: :controller do
|
||||
expect(response.media_type).to eq 'application/activity+json'
|
||||
end
|
||||
|
||||
it_behaves_like 'cachable response'
|
||||
it_behaves_like 'cacheable response'
|
||||
|
||||
context 'without only_other_accounts' do
|
||||
it "returns items with thread author's replies" do
|
||||
|
@ -31,7 +31,7 @@ describe Api::V1::Accounts::NotesController do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account note exceends allowed length' do
|
||||
context 'when account note exceeds allowed length' do
|
||||
let(:comment) { 'a' * 2_001 }
|
||||
|
||||
it 'returns 422' do
|
||||
|
@ -140,7 +140,7 @@ RSpec.describe Api::V1::Admin::AccountsController, type: :controller do
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it 'unsensitives account' do
|
||||
it 'unsensitizes account' do
|
||||
expect(account.reload.sensitized?).to be false
|
||||
end
|
||||
end
|
||||
|
@ -56,7 +56,7 @@ RSpec.describe Api::V1::Statuses::FavouritedByAccountsController, type: :control
|
||||
Fabricate(:favourite, status: status)
|
||||
end
|
||||
|
||||
it 'returns http unautharized' do
|
||||
it 'returns http unauthorized' do
|
||||
get :index, params: { status_id: status.id }
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
|
@ -56,7 +56,7 @@ RSpec.describe Api::V1::Statuses::RebloggedByAccountsController, type: :controll
|
||||
Fabricate(:status, reblog_of_id: status.id)
|
||||
end
|
||||
|
||||
it 'returns http unautharized' do
|
||||
it 'returns http unauthorized' do
|
||||
get :index, params: { status_id: status.id }
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
|
@ -130,7 +130,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
|
||||
let(:status) { Fabricate(:status, account: user.account, visibility: :private) }
|
||||
|
||||
describe 'GET #show' do
|
||||
it 'returns http unautharized' do
|
||||
it 'returns http unauthorized' do
|
||||
get :show, params: { id: status.id }
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
@ -141,7 +141,7 @@ RSpec.describe Api::V1::StatusesController, type: :controller do
|
||||
Fabricate(:status, account: user.account, thread: status)
|
||||
end
|
||||
|
||||
it 'returns http unautharized' do
|
||||
it 'returns http unauthorized' do
|
||||
get :context, params: { id: status.id }
|
||||
expect(response).to have_http_status(404)
|
||||
end
|
||||
|
@ -191,30 +191,30 @@ describe ApplicationController, type: :controller do
|
||||
controller do
|
||||
before_action :require_admin!
|
||||
|
||||
def sucesss
|
||||
def success
|
||||
head 200
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
routes.draw { get 'sucesss' => 'anonymous#sucesss' }
|
||||
routes.draw { get 'success' => 'anonymous#success' }
|
||||
end
|
||||
|
||||
it 'returns a 403 if current user is not admin' do
|
||||
sign_in(Fabricate(:user, admin: false))
|
||||
get 'sucesss'
|
||||
get 'success'
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
|
||||
it 'returns a 403 if current user is only a moderator' do
|
||||
sign_in(Fabricate(:user, moderator: true))
|
||||
get 'sucesss'
|
||||
get 'success'
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
|
||||
it 'does nothing if current user is admin' do
|
||||
sign_in(Fabricate(:user, admin: true))
|
||||
get 'sucesss'
|
||||
get 'success'
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
@ -223,30 +223,30 @@ describe ApplicationController, type: :controller do
|
||||
controller do
|
||||
before_action :require_staff!
|
||||
|
||||
def sucesss
|
||||
def success
|
||||
head 200
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
routes.draw { get 'sucesss' => 'anonymous#sucesss' }
|
||||
routes.draw { get 'success' => 'anonymous#success' }
|
||||
end
|
||||
|
||||
it 'returns a 403 if current user is not admin or moderator' do
|
||||
sign_in(Fabricate(:user, admin: false, moderator: false))
|
||||
get 'sucesss'
|
||||
get 'success'
|
||||
expect(response).to have_http_status(403)
|
||||
end
|
||||
|
||||
it 'does nothing if current user is moderator' do
|
||||
sign_in(Fabricate(:user, moderator: true))
|
||||
get 'sucesss'
|
||||
get 'success'
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
|
||||
it 'does nothing if current user is admin' do
|
||||
sign_in(Fabricate(:user, admin: true))
|
||||
get 'sucesss'
|
||||
get 'success'
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
@ -103,7 +103,7 @@ describe FollowerAccountsController do
|
||||
|
||||
context 'when account hides their network' do
|
||||
before do
|
||||
alice.user.settings.hide_network = true
|
||||
alice.update(hide_collections: true)
|
||||
end
|
||||
|
||||
it 'returns followers count' do
|
||||
|
@ -103,7 +103,7 @@ describe FollowingAccountsController do
|
||||
|
||||
context 'when account hides their network' do
|
||||
before do
|
||||
alice.user.settings.hide_network = true
|
||||
alice.update(hide_collections: true)
|
||||
end
|
||||
|
||||
it 'returns followers count' do
|
||||
|
@ -5,7 +5,7 @@ require 'rails_helper'
|
||||
describe StatusesController do
|
||||
render_views
|
||||
|
||||
shared_examples 'cachable response' do
|
||||
shared_examples 'cacheable response' do
|
||||
it 'does not set cookies' do
|
||||
expect(response.cookies).to be_empty
|
||||
expect(response.headers['Set-Cookies']).to be nil
|
||||
@ -108,7 +108,7 @@ describe StatusesController do
|
||||
expect(response.headers['Vary']).to eq 'Accept'
|
||||
end
|
||||
|
||||
it_behaves_like 'cachable response'
|
||||
it_behaves_like 'cacheable response'
|
||||
|
||||
it 'returns Content-Type header' do
|
||||
expect(response.headers['Content-Type']).to include 'application/activity+json'
|
||||
@ -496,7 +496,7 @@ describe StatusesController do
|
||||
expect(response.headers['Vary']).to eq 'Accept'
|
||||
end
|
||||
|
||||
it_behaves_like 'cachable response'
|
||||
it_behaves_like 'cacheable response'
|
||||
|
||||
it 'returns Content-Type header' do
|
||||
expect(response.headers['Content-Type']).to include 'application/activity+json'
|
||||
|
@ -60,7 +60,7 @@ describe ApplicationHelper do
|
||||
end
|
||||
|
||||
describe 'favicon_path' do
|
||||
it 'returns /favicon.ico on production enviromnent' do
|
||||
it 'returns /favicon.ico on production environment' do
|
||||
expect(Rails.env).to receive(:production?).and_return(true)
|
||||
expect(helper.favicon_path).to eq '/favicon.ico'
|
||||
end
|
||||
|
@ -6,7 +6,7 @@ RSpec.describe TagManager do
|
||||
|
||||
around do |example|
|
||||
original_local_domain = Rails.configuration.x.local_domain
|
||||
Rails.configuration.x.local_domain = 'domain.test'
|
||||
Rails.configuration.x.local_domain = 'domain.example.com'
|
||||
|
||||
example.run
|
||||
|
||||
@ -18,11 +18,11 @@ RSpec.describe TagManager do
|
||||
end
|
||||
|
||||
it 'returns true if the slash-stripped string equals to local domain' do
|
||||
expect(TagManager.instance.local_domain?('DoMaIn.Test/')).to eq true
|
||||
expect(TagManager.instance.local_domain?('DoMaIn.Example.com/')).to eq true
|
||||
end
|
||||
|
||||
it 'returns false for irrelevant string' do
|
||||
expect(TagManager.instance.local_domain?('DoMaIn.Test!')).to eq false
|
||||
expect(TagManager.instance.local_domain?('DoMaIn.Example.com!')).to eq false
|
||||
end
|
||||
end
|
||||
|
||||
@ -31,7 +31,7 @@ RSpec.describe TagManager do
|
||||
|
||||
around do |example|
|
||||
original_web_domain = Rails.configuration.x.web_domain
|
||||
Rails.configuration.x.web_domain = 'domain.test'
|
||||
Rails.configuration.x.web_domain = 'domain.example.com'
|
||||
|
||||
example.run
|
||||
|
||||
@ -43,11 +43,11 @@ RSpec.describe TagManager do
|
||||
end
|
||||
|
||||
it 'returns true if the slash-stripped string equals to web domain' do
|
||||
expect(TagManager.instance.web_domain?('DoMaIn.Test/')).to eq true
|
||||
expect(TagManager.instance.web_domain?('DoMaIn.Example.com/')).to eq true
|
||||
end
|
||||
|
||||
it 'returns false for string with irrelevant characters' do
|
||||
expect(TagManager.instance.web_domain?('DoMaIn.Test!')).to eq false
|
||||
expect(TagManager.instance.web_domain?('DoMaIn.Example.com!')).to eq false
|
||||
end
|
||||
end
|
||||
|
||||
@ -57,7 +57,7 @@ RSpec.describe TagManager do
|
||||
end
|
||||
|
||||
it 'returns normalized domain' do
|
||||
expect(TagManager.instance.normalize_domain('DoMaIn.Test/')).to eq 'domain.test'
|
||||
expect(TagManager.instance.normalize_domain('DoMaIn.Example.com/')).to eq 'domain.example.com'
|
||||
end
|
||||
end
|
||||
|
||||
@ -69,18 +69,18 @@ RSpec.describe TagManager do
|
||||
end
|
||||
|
||||
it 'returns true if the normalized string with port is local URL' do
|
||||
Rails.configuration.x.web_domain = 'domain.test:42'
|
||||
expect(TagManager.instance.local_url?('https://DoMaIn.Test:42/')).to eq true
|
||||
Rails.configuration.x.web_domain = 'domain.example.com:42'
|
||||
expect(TagManager.instance.local_url?('https://DoMaIn.Example.com:42/')).to eq true
|
||||
end
|
||||
|
||||
it 'returns true if the normalized string without port is local URL' do
|
||||
Rails.configuration.x.web_domain = 'domain.test'
|
||||
expect(TagManager.instance.local_url?('https://DoMaIn.Test/')).to eq true
|
||||
Rails.configuration.x.web_domain = 'domain.example.com'
|
||||
expect(TagManager.instance.local_url?('https://DoMaIn.Example.com/')).to eq true
|
||||
end
|
||||
|
||||
it 'returns false for string with irrelevant characters' do
|
||||
Rails.configuration.x.web_domain = 'domain.test'
|
||||
expect(TagManager.instance.local_url?('https://domainn.test/')).to eq false
|
||||
Rails.configuration.x.web_domain = 'domain.example.com'
|
||||
expect(TagManager.instance.local_url?('https://domain.example.net/')).to eq false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
229
spec/models/account_statuses_filter_spec.rb
Normal file
229
spec/models/account_statuses_filter_spec.rb
Normal file
@ -0,0 +1,229 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AccountStatusesFilter do
|
||||
let(:account) { Fabricate(:account) }
|
||||
let(:current_account) { nil }
|
||||
let(:params) { {} }
|
||||
|
||||
subject { described_class.new(account, current_account, params) }
|
||||
|
||||
def status!(visibility)
|
||||
Fabricate(:status, account: account, visibility: visibility)
|
||||
end
|
||||
|
||||
def status_with_tag!(visibility, tag)
|
||||
Fabricate(:status, account: account, visibility: visibility, tags: [tag])
|
||||
end
|
||||
|
||||
def status_with_parent!(visibility)
|
||||
Fabricate(:status, account: account, visibility: visibility, thread: Fabricate(:status))
|
||||
end
|
||||
|
||||
def status_with_reblog!(visibility)
|
||||
Fabricate(:status, account: account, visibility: visibility, reblog: Fabricate(:status))
|
||||
end
|
||||
|
||||
def status_with_mention!(visibility, mentioned_account = nil)
|
||||
Fabricate(:status, account: account, visibility: visibility).tap do |status|
|
||||
Fabricate(:mention, status: status, account: mentioned_account || Fabricate(:account))
|
||||
end
|
||||
end
|
||||
|
||||
def status_with_media_attachment!(visibility)
|
||||
Fabricate(:status, account: account, visibility: visibility).tap do |status|
|
||||
Fabricate(:media_attachment, account: account, status: status)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#results' do
|
||||
let(:tag) { Fabricate(:tag) }
|
||||
|
||||
before do
|
||||
status!(:public)
|
||||
status!(:unlisted)
|
||||
status!(:private)
|
||||
status_with_parent!(:public)
|
||||
status_with_reblog!(:public)
|
||||
status_with_tag!(:public, tag)
|
||||
status_with_mention!(:direct)
|
||||
status_with_media_attachment!(:public)
|
||||
end
|
||||
|
||||
shared_examples 'filter params' do
|
||||
context 'with only_media param' do
|
||||
let(:params) { { only_media: true } }
|
||||
|
||||
it 'returns only statuses with media' do
|
||||
expect(subject.results.all?(&:with_media?)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'with tagged param' do
|
||||
let(:params) { { tagged: tag.name } }
|
||||
|
||||
it 'returns only statuses with tag' do
|
||||
expect(subject.results.all? { |s| s.tags.include?(tag) }).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'with exclude_replies param' do
|
||||
let(:params) { { exclude_replies: true } }
|
||||
|
||||
it 'returns only statuses that are not replies' do
|
||||
expect(subject.results.none?(&:reply?)).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'with exclude_reblogs param' do
|
||||
let(:params) { { exclude_reblogs: true } }
|
||||
|
||||
it 'returns only statuses that are not reblogs' do
|
||||
expect(subject.results.none?(&:reblog?)).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when accessed anonymously' do
|
||||
let(:current_account) { nil }
|
||||
let(:direct_status) { nil }
|
||||
|
||||
it 'returns only public statuses' do
|
||||
expect(subject.results.pluck(:visibility).uniq).to match_array %w(unlisted public)
|
||||
end
|
||||
|
||||
it 'returns public replies' do
|
||||
expect(subject.results.pluck(:in_reply_to_id)).to_not be_empty
|
||||
end
|
||||
|
||||
it 'returns public reblogs' do
|
||||
expect(subject.results.pluck(:reblog_of_id)).to_not be_empty
|
||||
end
|
||||
|
||||
it_behaves_like 'filter params'
|
||||
end
|
||||
|
||||
context 'when accessed with a blocked account' do
|
||||
let(:current_account) { Fabricate(:account) }
|
||||
|
||||
before do
|
||||
account.block!(current_account)
|
||||
end
|
||||
|
||||
it 'returns nothing' do
|
||||
expect(subject.results.to_a).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when accessed by self' do
|
||||
let(:current_account) { account }
|
||||
|
||||
it 'returns everything' do
|
||||
expect(subject.results.pluck(:visibility).uniq).to match_array %w(direct private unlisted public)
|
||||
end
|
||||
|
||||
it 'returns replies' do
|
||||
expect(subject.results.pluck(:in_reply_to_id)).to_not be_empty
|
||||
end
|
||||
|
||||
it 'returns reblogs' do
|
||||
expect(subject.results.pluck(:reblog_of_id)).to_not be_empty
|
||||
end
|
||||
|
||||
it_behaves_like 'filter params'
|
||||
end
|
||||
|
||||
context 'when accessed by a follower' do
|
||||
let(:current_account) { Fabricate(:account) }
|
||||
|
||||
before do
|
||||
current_account.follow!(account)
|
||||
end
|
||||
|
||||
it 'returns private statuses' do
|
||||
expect(subject.results.pluck(:visibility).uniq).to match_array %w(private unlisted public)
|
||||
end
|
||||
|
||||
it 'returns replies' do
|
||||
expect(subject.results.pluck(:in_reply_to_id)).to_not be_empty
|
||||
end
|
||||
|
||||
it 'returns reblogs' do
|
||||
expect(subject.results.pluck(:reblog_of_id)).to_not be_empty
|
||||
end
|
||||
|
||||
context 'when there is a direct status mentioning the non-follower' do
|
||||
let!(:direct_status) { status_with_mention!(:direct, current_account) }
|
||||
|
||||
it 'returns the direct status' do
|
||||
expect(subject.results.pluck(:id)).to include(direct_status.id)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'filter params'
|
||||
end
|
||||
|
||||
context 'when accessed by a non-follower' do
|
||||
let(:current_account) { Fabricate(:account) }
|
||||
|
||||
it 'returns only public statuses' do
|
||||
expect(subject.results.pluck(:visibility).uniq).to match_array %w(unlisted public)
|
||||
end
|
||||
|
||||
it 'returns public replies' do
|
||||
expect(subject.results.pluck(:in_reply_to_id)).to_not be_empty
|
||||
end
|
||||
|
||||
it 'returns public reblogs' do
|
||||
expect(subject.results.pluck(:reblog_of_id)).to_not be_empty
|
||||
end
|
||||
|
||||
context 'when there is a private status mentioning the non-follower' do
|
||||
let!(:private_status) { status_with_mention!(:private, current_account) }
|
||||
|
||||
it 'returns the private status' do
|
||||
expect(subject.results.pluck(:id)).to include(private_status.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when blocking a reblogged account' do
|
||||
let(:reblog) { status_with_reblog!('public') }
|
||||
|
||||
before do
|
||||
current_account.block!(reblog.reblog.account)
|
||||
end
|
||||
|
||||
it 'does not return reblog of blocked account' do
|
||||
expect(subject.results.pluck(:id)).to_not include(reblog.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when muting a reblogged account' do
|
||||
let(:reblog) { status_with_reblog!('public') }
|
||||
|
||||
before do
|
||||
current_account.mute!(reblog.reblog.account)
|
||||
end
|
||||
|
||||
it 'does not return reblog of muted account' do
|
||||
expect(subject.results.pluck(:id)).to_not include(reblog.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when blocked by a reblogged account' do
|
||||
let(:reblog) { status_with_reblog!('public') }
|
||||
|
||||
before do
|
||||
reblog.reblog.account.block!(current_account)
|
||||
end
|
||||
|
||||
it 'does not return reblog of blocked-by account' do
|
||||
expect(subject.results.pluck(:id)).to_not include(reblog.id)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'filter params'
|
||||
end
|
||||
end
|
||||
end
|
@ -119,7 +119,7 @@ describe Report do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'validatiions' do
|
||||
describe 'validations' do
|
||||
it 'has a valid fabricator' do
|
||||
report = Fabricate(:report)
|
||||
report.valid?
|
||||
|
@ -435,59 +435,6 @@ RSpec.describe Status, type: :model do
|
||||
end
|
||||
end
|
||||
|
||||
describe '.permitted_for' do
|
||||
subject { described_class.permitted_for(target_account, account).pluck(:visibility) }
|
||||
|
||||
let(:target_account) { alice }
|
||||
let(:account) { bob }
|
||||
let!(:public_status) { Fabricate(:status, account: target_account, visibility: 'public') }
|
||||
let!(:unlisted_status) { Fabricate(:status, account: target_account, visibility: 'unlisted') }
|
||||
let!(:private_status) { Fabricate(:status, account: target_account, visibility: 'private') }
|
||||
|
||||
let!(:direct_status) do
|
||||
Fabricate(:status, account: target_account, visibility: 'direct').tap do |status|
|
||||
Fabricate(:mention, status: status, account: account)
|
||||
end
|
||||
end
|
||||
|
||||
let!(:other_direct_status) do
|
||||
Fabricate(:status, account: target_account, visibility: 'direct').tap do |status|
|
||||
Fabricate(:mention, status: status)
|
||||
end
|
||||
end
|
||||
|
||||
context 'given nil' do
|
||||
let(:account) { nil }
|
||||
let(:direct_status) { nil }
|
||||
it { is_expected.to eq(%w(unlisted public)) }
|
||||
end
|
||||
|
||||
context 'given blocked account' do
|
||||
before do
|
||||
target_account.block!(account)
|
||||
end
|
||||
|
||||
it { is_expected.to be_empty }
|
||||
end
|
||||
|
||||
context 'given same account' do
|
||||
let(:account) { target_account }
|
||||
it { is_expected.to eq(%w(direct direct private unlisted public)) }
|
||||
end
|
||||
|
||||
context 'given followed account' do
|
||||
before do
|
||||
account.follow!(target_account)
|
||||
end
|
||||
|
||||
it { is_expected.to eq(%w(direct private unlisted public)) }
|
||||
end
|
||||
|
||||
context 'given unfollowed account' do
|
||||
it { is_expected.to eq(%w(direct unlisted public)) }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'before_validation' do
|
||||
it 'sets account being replied to correctly over intermediary nodes' do
|
||||
first_status = Fabricate(:status, account: bob)
|
||||
|
@ -114,13 +114,13 @@ RSpec.describe UserPolicy do
|
||||
|
||||
permissions :promote? do
|
||||
context 'admin?' do
|
||||
context 'promoteable?' do
|
||||
context 'promotable?' do
|
||||
it 'permits' do
|
||||
expect(subject).to permit(admin, john.user)
|
||||
end
|
||||
end
|
||||
|
||||
context '!promoteable?' do
|
||||
context '!promotable?' do
|
||||
it 'denies' do
|
||||
expect(subject).to_not permit(admin, admin.user)
|
||||
end
|
||||
|
58
spec/presenters/familiar_followers_presenter_spec.rb
Normal file
58
spec/presenters/familiar_followers_presenter_spec.rb
Normal file
@ -0,0 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe FamiliarFollowersPresenter do
|
||||
describe '#accounts' do
|
||||
let(:account) { Fabricate(:account) }
|
||||
let(:familiar_follower) { Fabricate(:account) }
|
||||
let(:requested_accounts) { Fabricate.times(2, :account) }
|
||||
|
||||
subject { described_class.new(requested_accounts, account.id) }
|
||||
|
||||
before do
|
||||
familiar_follower.follow!(requested_accounts.first)
|
||||
account.follow!(familiar_follower)
|
||||
end
|
||||
|
||||
it 'returns a result for each requested account' do
|
||||
expect(subject.accounts.map(&:id)).to eq requested_accounts.map(&:id)
|
||||
end
|
||||
|
||||
it 'returns followers you follow' do
|
||||
result = subject.accounts.first
|
||||
|
||||
expect(result).to_not be_nil
|
||||
expect(result.id).to eq requested_accounts.first.id
|
||||
expect(result.accounts).to match_array([familiar_follower])
|
||||
end
|
||||
|
||||
context 'when requested account hides followers' do
|
||||
before do
|
||||
requested_accounts.first.update(hide_collections: true)
|
||||
end
|
||||
|
||||
it 'does not return followers you follow' do
|
||||
result = subject.accounts.first
|
||||
|
||||
expect(result).to_not be_nil
|
||||
expect(result.id).to eq requested_accounts.first.id
|
||||
expect(result.accounts).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when familiar follower hides follows' do
|
||||
before do
|
||||
familiar_follower.update(hide_collections: true)
|
||||
end
|
||||
|
||||
it 'does not return followers you follow' do
|
||||
result = subject.accounts.first
|
||||
|
||||
expect(result).to_not be_nil
|
||||
expect(result.id).to eq requested_accounts.first.id
|
||||
expect(result.accounts).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -63,20 +63,20 @@ RSpec.describe UnsuspendAccountService, type: :service do
|
||||
describe 'unsuspending a remote account' do
|
||||
include_examples 'common behavior' do
|
||||
let!(:account) { Fabricate(:account, domain: 'bob.com', uri: 'https://bob.com', inbox_url: 'https://bob.com/inbox', protocol: :activitypub) }
|
||||
let!(:reslove_account_service) { double }
|
||||
let!(:resolve_account_service) { double }
|
||||
|
||||
before do
|
||||
allow(ResolveAccountService).to receive(:new).and_return(reslove_account_service)
|
||||
allow(ResolveAccountService).to receive(:new).and_return(resolve_account_service)
|
||||
end
|
||||
|
||||
context 'when the account is not remotely suspended' do
|
||||
before do
|
||||
allow(reslove_account_service).to receive(:call).with(account).and_return(account)
|
||||
allow(resolve_account_service).to receive(:call).with(account).and_return(account)
|
||||
end
|
||||
|
||||
it 're-fetches the account' do
|
||||
subject.call
|
||||
expect(reslove_account_service).to have_received(:call).with(account)
|
||||
expect(resolve_account_service).to have_received(:call).with(account)
|
||||
end
|
||||
|
||||
it "merges back into local followers' feeds" do
|
||||
@ -92,7 +92,7 @@ RSpec.describe UnsuspendAccountService, type: :service do
|
||||
|
||||
context 'when the account is remotely suspended' do
|
||||
before do
|
||||
allow(reslove_account_service).to receive(:call).with(account) do |account|
|
||||
allow(resolve_account_service).to receive(:call).with(account) do |account|
|
||||
account.suspend!(origin: :remote)
|
||||
account
|
||||
end
|
||||
@ -100,7 +100,7 @@ RSpec.describe UnsuspendAccountService, type: :service do
|
||||
|
||||
it 're-fetches the account' do
|
||||
subject.call
|
||||
expect(reslove_account_service).to have_received(:call).with(account)
|
||||
expect(resolve_account_service).to have_received(:call).with(account)
|
||||
end
|
||||
|
||||
it "does not merge back into local followers' feeds" do
|
||||
@ -116,12 +116,12 @@ RSpec.describe UnsuspendAccountService, type: :service do
|
||||
|
||||
context 'when the account is remotely deleted' do
|
||||
before do
|
||||
allow(reslove_account_service).to receive(:call).with(account).and_return(nil)
|
||||
allow(resolve_account_service).to receive(:call).with(account).and_return(nil)
|
||||
end
|
||||
|
||||
it 're-fetches the account' do
|
||||
subject.call
|
||||
expect(reslove_account_service).to have_received(:call).with(account)
|
||||
expect(resolve_account_service).to have_received(:call).with(account)
|
||||
end
|
||||
|
||||
it "does not merge back into local followers' feeds" do
|
||||
|
@ -22,7 +22,7 @@ module ProfileStories
|
||||
def with_alice_as_local_user
|
||||
@alice_bio = '@alice and @bob are fictional characters commonly used as'\
|
||||
'placeholder names in #cryptology, as well as #science and'\
|
||||
'engineering 📖 literature. Not affilated with @pepe.'
|
||||
'engineering 📖 literature. Not affiliated with @pepe.'
|
||||
|
||||
@alice = Fabricate(
|
||||
:user,
|
||||
|
Loading…
Reference in New Issue
Block a user