Merge remote-tracking branch 'origin/develop' into admin_cfg
This commit is contained in:
commit
c3be47fca6
53 changed files with 1018 additions and 110 deletions
|
@ -54,7 +54,7 @@ yarn
|
|||
Finally, run the dev server:
|
||||
|
||||
```sh
|
||||
yarn start
|
||||
yarn dev
|
||||
```
|
||||
|
||||
**That's it!** :tada:
|
||||
|
@ -140,7 +140,7 @@ NODE_ENV=development
|
|||
```
|
||||
|
||||
#### Local dev server
|
||||
- `yarn dev` - Exact same as above, aliased to `yarn start` for convenience.
|
||||
- `yarn dev` - Run the local dev server.
|
||||
|
||||
#### Building
|
||||
- `yarn build` - Compile without a dev server, into `/static` directory.
|
||||
|
|
|
@ -836,6 +836,7 @@
|
|||
"registration.lead": "With an account on {instance} you\"ll be able to follow people on any server in the fediverse.",
|
||||
"registration.sign_up": "Sign up",
|
||||
"registration.tos": "Terms of Service",
|
||||
"registration.reason": "Reason for Joining",
|
||||
"relative_time.days": "{number}d",
|
||||
"relative_time.hours": "{number}h",
|
||||
"relative_time.just_now": "now",
|
||||
|
|
|
@ -133,15 +133,20 @@ export function logOut() {
|
|||
export function register(params) {
|
||||
return (dispatch, getState) => {
|
||||
const needsConfirmation = getState().getIn(['instance', 'pleroma', 'metadata', 'account_activation_required']);
|
||||
const needsApproval = getState().getIn(['instance', 'approval_required']);
|
||||
dispatch({ type: AUTH_REGISTER_REQUEST });
|
||||
return dispatch(createAppAndToken()).then(() => {
|
||||
return api(getState, 'app').post('/api/v1/accounts', params);
|
||||
}).then(response => {
|
||||
dispatch({ type: AUTH_REGISTER_SUCCESS, token: response.data });
|
||||
dispatch(authLoggedIn(response.data));
|
||||
return needsConfirmation
|
||||
? dispatch(showAlert('', 'Check your email for further instructions.'))
|
||||
: dispatch(fetchMe());
|
||||
if (needsConfirmation) {
|
||||
return dispatch(showAlert('', 'Check your email for further instructions.'));
|
||||
} else if (needsApproval) {
|
||||
return dispatch(showAlert('', 'Your account has been submitted for approval.'));
|
||||
} else {
|
||||
return dispatch(fetchMe());
|
||||
}
|
||||
}).catch(error => {
|
||||
dispatch({ type: AUTH_REGISTER_FAIL, error });
|
||||
throw error;
|
||||
|
|
90
app/soapbox/actions/bookmarks.js
Normal file
90
app/soapbox/actions/bookmarks.js
Normal file
|
@ -0,0 +1,90 @@
|
|||
import api, { getLinks } from '../api';
|
||||
import { importFetchedStatuses } from './importer';
|
||||
|
||||
export const BOOKMARKED_STATUSES_FETCH_REQUEST = 'BOOKMARKED_STATUSES_FETCH_REQUEST';
|
||||
export const BOOKMARKED_STATUSES_FETCH_SUCCESS = 'BOOKMARKED_STATUSES_FETCH_SUCCESS';
|
||||
export const BOOKMARKED_STATUSES_FETCH_FAIL = 'BOOKMARKED_STATUSES_FETCH_FAIL';
|
||||
|
||||
export const BOOKMARKED_STATUSES_EXPAND_REQUEST = 'BOOKMARKED_STATUSES_EXPAND_REQUEST';
|
||||
export const BOOKMARKED_STATUSES_EXPAND_SUCCESS = 'BOOKMARKED_STATUSES_EXPAND_SUCCESS';
|
||||
export const BOOKMARKED_STATUSES_EXPAND_FAIL = 'BOOKMARKED_STATUSES_EXPAND_FAIL';
|
||||
|
||||
export function fetchBookmarkedStatuses() {
|
||||
return (dispatch, getState) => {
|
||||
if (getState().getIn(['status_lists', 'bookmarks', 'isLoading'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(fetchBookmarkedStatusesRequest());
|
||||
|
||||
api(getState).get('/api/v1/bookmarks').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
|
||||
}).catch(error => {
|
||||
dispatch(fetchBookmarkedStatusesFail(error));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchBookmarkedStatusesRequest() {
|
||||
return {
|
||||
type: BOOKMARKED_STATUSES_FETCH_REQUEST,
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchBookmarkedStatusesSuccess(statuses, next) {
|
||||
return {
|
||||
type: BOOKMARKED_STATUSES_FETCH_SUCCESS,
|
||||
statuses,
|
||||
next,
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchBookmarkedStatusesFail(error) {
|
||||
return {
|
||||
type: BOOKMARKED_STATUSES_FETCH_FAIL,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
export function expandBookmarkedStatuses() {
|
||||
return (dispatch, getState) => {
|
||||
const url = getState().getIn(['status_lists', 'bookmarks', 'next'], null);
|
||||
|
||||
if (url === null || getState().getIn(['status_lists', 'bookmarks', 'isLoading'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(expandBookmarkedStatusesRequest());
|
||||
|
||||
api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
|
||||
}).catch(error => {
|
||||
dispatch(expandBookmarkedStatusesFail(error));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export function expandBookmarkedStatusesRequest() {
|
||||
return {
|
||||
type: BOOKMARKED_STATUSES_EXPAND_REQUEST,
|
||||
};
|
||||
};
|
||||
|
||||
export function expandBookmarkedStatusesSuccess(statuses, next) {
|
||||
return {
|
||||
type: BOOKMARKED_STATUSES_EXPAND_SUCCESS,
|
||||
statuses,
|
||||
next,
|
||||
};
|
||||
};
|
||||
|
||||
export function expandBookmarkedStatusesFail(error) {
|
||||
return {
|
||||
type: BOOKMARKED_STATUSES_EXPAND_FAIL,
|
||||
error,
|
||||
};
|
||||
};
|
|
@ -43,6 +43,7 @@ export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
|
|||
|
||||
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
|
||||
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
|
||||
export const COMPOSE_TYPE_CHANGE = 'COMPOSE_TYPE_CHANGE';
|
||||
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
|
||||
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
|
||||
export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE';
|
||||
|
@ -175,6 +176,7 @@ export function submitCompose(routerHistory, group) {
|
|||
sensitive: getState().getIn(['compose', 'sensitive']),
|
||||
spoiler_text: getState().getIn(['compose', 'spoiler_text'], ''),
|
||||
visibility: getState().getIn(['compose', 'privacy']),
|
||||
content_type: getState().getIn(['compose', 'content_type']),
|
||||
poll: getState().getIn(['compose', 'poll'], null),
|
||||
group_id: group ? group.get('id') : null,
|
||||
}, {
|
||||
|
@ -226,11 +228,6 @@ export function uploadCompose(files) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (getState().getIn(['compose', 'poll'])) {
|
||||
dispatch(showAlert(undefined, messages.uploadErrorPoll));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(uploadComposeRequest());
|
||||
|
||||
for (const [i, f] of Array.from(files).entries()) {
|
||||
|
@ -495,6 +492,13 @@ export function changeComposeSpoilerness() {
|
|||
};
|
||||
};
|
||||
|
||||
export function changeComposeContentType(value) {
|
||||
return {
|
||||
type: COMPOSE_TYPE_CHANGE,
|
||||
value,
|
||||
};
|
||||
};
|
||||
|
||||
export function changeComposeSpoilerText(text) {
|
||||
return {
|
||||
type: COMPOSE_SPOILER_TEXT_CHANGE,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import api from '../api';
|
||||
import { importFetchedAccounts, importFetchedStatus } from './importer';
|
||||
import { showAlert } from 'soapbox/actions/alerts';
|
||||
|
||||
export const REBLOG_REQUEST = 'REBLOG_REQUEST';
|
||||
export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
|
||||
|
@ -33,6 +34,14 @@ export const UNPIN_REQUEST = 'UNPIN_REQUEST';
|
|||
export const UNPIN_SUCCESS = 'UNPIN_SUCCESS';
|
||||
export const UNPIN_FAIL = 'UNPIN_FAIL';
|
||||
|
||||
export const BOOKMARK_REQUEST = 'BOOKMARK_REQUEST';
|
||||
export const BOOKMARK_SUCCESS = 'BOOKMARKED_SUCCESS';
|
||||
export const BOOKMARK_FAIL = 'BOOKMARKED_FAIL';
|
||||
|
||||
export const UNBOOKMARK_REQUEST = 'UNBOOKMARKED_REQUEST';
|
||||
export const UNBOOKMARK_SUCCESS = 'UNBOOKMARKED_SUCCESS';
|
||||
export const UNBOOKMARK_FAIL = 'UNBOOKMARKED_FAIL';
|
||||
|
||||
export function reblog(status) {
|
||||
return function(dispatch, getState) {
|
||||
if (!getState().get('me')) return;
|
||||
|
@ -195,6 +204,80 @@ export function unfavouriteFail(status, error) {
|
|||
};
|
||||
};
|
||||
|
||||
export function bookmark(status) {
|
||||
return function(dispatch, getState) {
|
||||
dispatch(bookmarkRequest(status));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/bookmark`).then(function(response) {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(bookmarkSuccess(status, response.data));
|
||||
dispatch(showAlert('', 'Bookmark added'));
|
||||
}).catch(function(error) {
|
||||
dispatch(bookmarkFail(status, error));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export function unbookmark(status) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(unbookmarkRequest(status));
|
||||
|
||||
api(getState).post(`/api/v1/statuses/${status.get('id')}/unbookmark`).then(response => {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(unbookmarkSuccess(status, response.data));
|
||||
dispatch(showAlert('', 'Bookmark removed'));
|
||||
}).catch(error => {
|
||||
dispatch(unbookmarkFail(status, error));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export function bookmarkRequest(status) {
|
||||
return {
|
||||
type: BOOKMARK_REQUEST,
|
||||
status: status,
|
||||
};
|
||||
};
|
||||
|
||||
export function bookmarkSuccess(status, response) {
|
||||
return {
|
||||
type: BOOKMARK_SUCCESS,
|
||||
status: status,
|
||||
response: response,
|
||||
};
|
||||
};
|
||||
|
||||
export function bookmarkFail(status, error) {
|
||||
return {
|
||||
type: BOOKMARK_FAIL,
|
||||
status: status,
|
||||
error: error,
|
||||
};
|
||||
};
|
||||
|
||||
export function unbookmarkRequest(status) {
|
||||
return {
|
||||
type: UNBOOKMARK_REQUEST,
|
||||
status: status,
|
||||
};
|
||||
};
|
||||
|
||||
export function unbookmarkSuccess(status, response) {
|
||||
return {
|
||||
type: UNBOOKMARK_SUCCESS,
|
||||
status: status,
|
||||
response: response,
|
||||
};
|
||||
};
|
||||
|
||||
export function unbookmarkFail(status, error) {
|
||||
return {
|
||||
type: UNBOOKMARK_FAIL,
|
||||
status: status,
|
||||
error: error,
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchReblogs(id) {
|
||||
return (dispatch, getState) => {
|
||||
if (!getState().get('me')) return;
|
||||
|
|
|
@ -22,6 +22,7 @@ const defaultSettings = ImmutableMap({
|
|||
defaultPrivacy: 'public',
|
||||
themeMode: 'light',
|
||||
locale: navigator.language.split(/[-_]/)[0] || 'en',
|
||||
explanationBox: true,
|
||||
|
||||
systemFont: false,
|
||||
dyslexicFont: false,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import VerificationBadge from './verification_badge';
|
||||
import { acctFull } from '../utils/accounts';
|
||||
|
@ -8,10 +9,11 @@ export default class DisplayName extends React.PureComponent {
|
|||
static propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
others: ImmutablePropTypes.list,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { account, others } = this.props;
|
||||
const { account, others, children } = this.props;
|
||||
|
||||
let displayName, suffix;
|
||||
|
||||
|
@ -40,6 +42,7 @@ export default class DisplayName extends React.PureComponent {
|
|||
<span className='display-name'>
|
||||
{displayName}
|
||||
{suffix}
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@ const messages = defineMessages({
|
|||
security: { id: 'navigation_bar.security', defaultMessage: 'Security' },
|
||||
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||
lists: { id: 'column.lists', defaultMessage: 'Lists' },
|
||||
bookmarks: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
||||
apps: { id: 'tabs_bar.apps', defaultMessage: 'Apps' },
|
||||
news: { id: 'tabs_bar.news', defaultMessage: 'News' },
|
||||
donate: { id: 'donate', defaultMessage: 'Donate' },
|
||||
|
@ -145,6 +146,10 @@ class SidebarMenu extends ImmutablePureComponent {
|
|||
<Icon id='list' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.lists)}</span>
|
||||
</NavLink>
|
||||
<NavLink className='sidebar-menu-item' to='/bookmarks' onClick={onClose}>
|
||||
<Icon id='bookmark' />
|
||||
<span className='sidebar-menu-item__title'>{intl.formatMessage(messages.bookmarks)}</span>
|
||||
</NavLink>
|
||||
</div>
|
||||
|
||||
<div className='sidebar-menu__section'>
|
||||
|
|
|
@ -18,6 +18,9 @@ import classNames from 'classnames';
|
|||
import Icon from 'soapbox/components/icon';
|
||||
import PollContainer from 'soapbox/containers/poll_container';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import ProfileHoverCardContainer from '../features/profile_hover_card/profile_hover_card_container';
|
||||
import { isMobile } from '../../../app/soapbox/is_mobile';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
// We use the component (and not the container) since we do not want
|
||||
// to use the progress bar to show download progress
|
||||
|
@ -104,6 +107,7 @@ class Status extends ImmutablePureComponent {
|
|||
state = {
|
||||
showMedia: defaultMediaVisibility(this.props.status, this.props.displayMedia),
|
||||
statusId: undefined,
|
||||
profileCardVisible: false,
|
||||
};
|
||||
|
||||
// Track height changes we know about to compensate scrolling
|
||||
|
@ -249,6 +253,19 @@ class Status extends ImmutablePureComponent {
|
|||
this.handleToggleMediaVisibility();
|
||||
}
|
||||
|
||||
showProfileCard = debounce(() => {
|
||||
this.setState({ profileCardVisible: true });
|
||||
}, 1200);
|
||||
|
||||
handleProfileHover = e => {
|
||||
if (!isMobile(window.innerWidth)) this.showProfileCard();
|
||||
}
|
||||
|
||||
handleProfileLeave = e => {
|
||||
this.showProfileCard.cancel();
|
||||
this.setState({ profileCardVisible: false });
|
||||
}
|
||||
|
||||
_properStatus() {
|
||||
const { status } = this.props;
|
||||
|
||||
|
@ -265,6 +282,7 @@ class Status extends ImmutablePureComponent {
|
|||
|
||||
render() {
|
||||
let media = null;
|
||||
let poll = null;
|
||||
let statusAvatar, prepend, rebloggedByText, reblogContent;
|
||||
|
||||
const { intl, hidden, featured, otherAccounts, unread, showThread, group } = this.props;
|
||||
|
@ -332,8 +350,9 @@ class Status extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
if (status.get('poll')) {
|
||||
media = <PollContainer pollId={status.get('poll')} />;
|
||||
} else if (status.get('media_attachments').size > 0) {
|
||||
poll = <PollContainer pollId={status.get('poll')} />;
|
||||
}
|
||||
if (status.get('media_attachments').size > 0) {
|
||||
if (this.props.muted) {
|
||||
media = (
|
||||
<AttachmentList
|
||||
|
@ -435,6 +454,7 @@ class Status extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
const statusUrl = `/@${status.getIn(['account', 'acct'])}/posts/${status.get('id')}`;
|
||||
const { profileCardVisible } = this.state;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={handlers}>
|
||||
|
@ -448,13 +468,16 @@ class Status extends ImmutablePureComponent {
|
|||
<RelativeTimestamp timestamp={status.get('created_at')} />
|
||||
</NavLink>
|
||||
|
||||
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name'>
|
||||
<div className='status__profile' onMouseEnter={this.handleProfileHover} onMouseLeave={this.handleProfileLeave}>
|
||||
<div className='status__avatar'>
|
||||
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='floating-link' />
|
||||
{statusAvatar}
|
||||
</div>
|
||||
|
||||
<DisplayName account={status.get('account')} others={otherAccounts} />
|
||||
</NavLink>
|
||||
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='status__display-name'>
|
||||
<DisplayName account={status.get('account')} others={otherAccounts} />
|
||||
</NavLink>
|
||||
<ProfileHoverCardContainer accountId={status.getIn(['account', 'id'])} visible={!isMobile(window.innerWidth) && profileCardVisible} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!group && status.get('group') && (
|
||||
|
@ -473,6 +496,7 @@ class Status extends ImmutablePureComponent {
|
|||
/>
|
||||
|
||||
{media}
|
||||
{poll}
|
||||
|
||||
{showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && (
|
||||
<button className='status__content__read-more-button' onClick={this.handleClick}>
|
||||
|
|
|
@ -31,6 +31,8 @@ const messages = defineMessages({
|
|||
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be reposted' },
|
||||
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' },
|
||||
open: { id: 'status.open', defaultMessage: 'Expand this post' },
|
||||
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
|
||||
unbookmark: { id: 'status.unbookmark', defaultMessage: 'Remove bookmark' },
|
||||
report: { id: 'status.report', defaultMessage: 'Report @{name}' },
|
||||
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
|
||||
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
|
||||
|
@ -55,6 +57,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
onOpenUnauthorizedModal: PropTypes.func.isRequired,
|
||||
onReply: PropTypes.func,
|
||||
onFavourite: PropTypes.func,
|
||||
onBookmark: PropTypes.func,
|
||||
onReblog: PropTypes.func,
|
||||
onDelete: PropTypes.func,
|
||||
onDirect: PropTypes.func,
|
||||
|
@ -149,6 +152,10 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleBookmarkClick = () => {
|
||||
this.props.onBookmark(this.props.status);
|
||||
}
|
||||
|
||||
handleReblogClick = e => {
|
||||
const { me } = this.props;
|
||||
if (me) {
|
||||
|
@ -246,6 +253,8 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
// menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
||||
}
|
||||
|
||||
menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.unbookmark : messages.bookmark), action: this.handleBookmarkClick });
|
||||
|
||||
if (!me) {
|
||||
return menu;
|
||||
}
|
||||
|
|
|
@ -66,7 +66,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
}
|
||||
},
|
||||
|
||||
|
||||
onMuteNotifications(account, notifications) {
|
||||
dispatch(muteAccount(account.get('id'), notifications));
|
||||
},
|
||||
|
|
|
@ -12,6 +12,8 @@ import {
|
|||
favourite,
|
||||
unreblog,
|
||||
unfavourite,
|
||||
bookmark,
|
||||
unbookmark,
|
||||
pin,
|
||||
unpin,
|
||||
} from '../actions/interactions';
|
||||
|
@ -100,6 +102,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
|||
}
|
||||
},
|
||||
|
||||
onBookmark(status) {
|
||||
if (status.get('bookmarked')) {
|
||||
dispatch(unbookmark(status));
|
||||
} else {
|
||||
dispatch(bookmark(status));
|
||||
}
|
||||
},
|
||||
|
||||
onPin(status) {
|
||||
if (status.get('pinned')) {
|
||||
dispatch(unpin(status));
|
||||
|
|
|
@ -17,12 +17,9 @@ import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container';
|
|||
import ProfileInfoPanel from '../../ui/components/profile_info_panel';
|
||||
import { debounce } from 'lodash';
|
||||
import StillImage from 'soapbox/components/still_image';
|
||||
import ActionButton from 'soapbox/features/ui/components/action_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
|
||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||
linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
|
||||
account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' },
|
||||
|
@ -65,8 +62,6 @@ class Header extends ImmutablePureComponent {
|
|||
static propTypes = {
|
||||
account: ImmutablePropTypes.map,
|
||||
identity_props: ImmutablePropTypes.list,
|
||||
onFollow: PropTypes.func.isRequired,
|
||||
onBlock: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
username: PropTypes.string,
|
||||
isStaff: PropTypes.bool.isRequired,
|
||||
|
@ -199,30 +194,6 @@ class Header extends ImmutablePureComponent {
|
|||
return info;
|
||||
};
|
||||
|
||||
getActionBtn() {
|
||||
const { account, intl, me } = this.props;
|
||||
|
||||
let actionBtn = null;
|
||||
|
||||
if (!account || !me) return actionBtn;
|
||||
|
||||
if (me !== account.get('id')) {
|
||||
if (!account.get('relationship')) { // Wait until the relationship is loaded
|
||||
//
|
||||
} else if (account.getIn(['relationship', 'requested'])) {
|
||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
|
||||
} 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.props.onFollow} />;
|
||||
} else if (account.getIn(['relationship', 'blocking'])) {
|
||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
|
||||
}
|
||||
} else {
|
||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} to='/settings/profile' />;
|
||||
}
|
||||
|
||||
return actionBtn;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { account, intl, username, me } = this.props;
|
||||
const { isSmallScreen } = this.state;
|
||||
|
@ -247,7 +218,6 @@ class Header extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
const info = this.makeInfo();
|
||||
const actionBtn = this.getActionBtn();
|
||||
const menu = this.makeMenu();
|
||||
|
||||
const headerMissing = (account.get('header').indexOf('/headers/original/missing.png') > -1);
|
||||
|
@ -319,7 +289,7 @@ class Header extends ImmutablePureComponent {
|
|||
{
|
||||
me &&
|
||||
<div className='account__header__extra__buttons'>
|
||||
{actionBtn}
|
||||
<ActionButton account={account} />
|
||||
{account.get('id') !== me &&
|
||||
<Button className='button button-alternative-2' onClick={this.props.onDirect}>
|
||||
<FormattedMessage
|
||||
|
|
74
app/soapbox/features/bookmarks/index.js
Normal file
74
app/soapbox/features/bookmarks/index.js
Normal file
|
@ -0,0 +1,74 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Column from '../ui/components/column';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import StatusList from '../../components/status_list';
|
||||
import { fetchBookmarkedStatuses, expandBookmarkedStatuses } from '../../actions/bookmarks';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
statusIds: state.getIn(['status_lists', 'bookmarks', 'items']),
|
||||
isLoading: state.getIn(['status_lists', 'bookmarks', 'isLoading'], true),
|
||||
hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Bookmarks extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
shouldUpdateScroll: PropTypes.func,
|
||||
statusIds: ImmutablePropTypes.list.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
columnId: PropTypes.string,
|
||||
multiColumn: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(fetchBookmarkedStatuses());
|
||||
}
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandBookmarkedStatuses());
|
||||
}, 300, { leading: true })
|
||||
|
||||
|
||||
render() {
|
||||
const { intl, shouldUpdateScroll, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
|
||||
const pinned = !!columnId;
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.bookmarks' defaultMessage="You don't have any bookmarks yet. When you add one, it will show up here." />;
|
||||
|
||||
return (
|
||||
<Column icon='bookmark' heading={intl.formatMessage(messages.heading)} backBtnSlim>
|
||||
<StatusList
|
||||
trackScroll={!pinned}
|
||||
statusIds={statusIds}
|
||||
scrollKey={`bookmarked_statuses-${columnId}`}
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
shouldUpdateScroll={shouldUpdateScroll}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -11,6 +11,7 @@ const messages = defineMessages({
|
|||
profile: { id: 'account.profile', defaultMessage: 'Profile' },
|
||||
messages: { id: 'navigation_bar.messages', defaultMessage: 'Messages' },
|
||||
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
|
||||
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
|
||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
||||
|
@ -70,6 +71,7 @@ class ActionBar extends React.PureComponent {
|
|||
menu.push({ text: intl.formatMessage(messages.profile), to: `/@${meUsername}` });
|
||||
menu.push({ text: intl.formatMessage(messages.messages), to: '/messages' });
|
||||
menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
|
||||
menu.push({ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' });
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
|
||||
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
|
||||
|
|
|
@ -11,6 +11,7 @@ import PollButtonContainer from '../containers/poll_button_container';
|
|||
import UploadButtonContainer from '../containers/upload_button_container';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import SpoilerButtonContainer from '../containers/spoiler_button_container';
|
||||
import MarkdownButtonContainer from '../containers/markdown_button_container';
|
||||
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
|
||||
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
|
||||
import PollFormContainer from '../containers/poll_form_container';
|
||||
|
@ -303,6 +304,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
<PollButtonContainer />
|
||||
<PrivacyDropdownContainer />
|
||||
<SpoilerButtonContainer />
|
||||
<MarkdownButtonContainer />
|
||||
</div>
|
||||
{maxTootChars && <div className='character-counter__wrapper'><CharacterCounter max={maxTootChars} text={text} /></div>}
|
||||
<div className='compose-form__publish'>
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import { connect } from 'react-redux';
|
||||
import TextIconButton from '../components/text_icon_button';
|
||||
import { changeComposeContentType } from '../../../actions/compose';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
|
||||
const messages = defineMessages({
|
||||
marked: { id: 'compose_form.markdown.marked', defaultMessage: 'Post markdown enabled' },
|
||||
unmarked: { id: 'compose_form.markdown.unmarked', defaultMessage: 'Post markdown disabled' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { intl }) => ({
|
||||
label: 'MD',
|
||||
title: intl.formatMessage(state.getIn(['compose', 'content_type']) === 'text/markdown' ? messages.marked : messages.unmarked),
|
||||
active: state.getIn(['compose', 'content_type']) === 'text/markdown',
|
||||
ariaControls: 'markdown-input',
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onClick() {
|
||||
dispatch(changeComposeContentType(this.active ? 'text/plain' : 'text/markdown'));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(TextIconButton));
|
|
@ -3,7 +3,7 @@ import PollButton from '../components/poll_button';
|
|||
import { addPoll, removePoll } from '../../../actions/compose';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
unavailable: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 0),
|
||||
unavailable: state.getIn(['compose', 'is_uploading']),
|
||||
active: state.getIn(['compose', 'poll']) !== null,
|
||||
});
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ const messages = defineMessages({
|
|||
|
||||
const mapStateToProps = state => ({
|
||||
active: state.getIn(['compose', 'sensitive']),
|
||||
disabled: state.getIn(['compose', 'spoiler']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
@ -26,12 +27,13 @@ class SensitiveButton extends React.PureComponent {
|
|||
|
||||
static propTypes = {
|
||||
active: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { active, onClick, intl } = this.props;
|
||||
const { active, disabled, onClick, intl } = this.props;
|
||||
|
||||
return (
|
||||
<div className='compose-form__sensitive-button'>
|
||||
|
@ -41,6 +43,7 @@ class SensitiveButton extends React.PureComponent {
|
|||
type='checkbox'
|
||||
checked={active}
|
||||
onChange={onClick}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<span className={classNames('checkbox', { active })} />
|
||||
|
|
|
@ -4,7 +4,6 @@ import { uploadCompose } from '../../../actions/compose';
|
|||
|
||||
const mapStateToProps = state => ({
|
||||
disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')),
|
||||
unavailable: state.getIn(['compose', 'poll']) !== null,
|
||||
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
||||
});
|
||||
|
||||
|
|
|
@ -74,6 +74,17 @@ LabelInput.propTypes = {
|
|||
dispatch: PropTypes.func,
|
||||
};
|
||||
|
||||
export const LabelTextarea = ({ label, dispatch, ...props }) => (
|
||||
<LabelInputContainer label={label}>
|
||||
<textarea {...props} />
|
||||
</LabelInputContainer>
|
||||
);
|
||||
|
||||
LabelTextarea.propTypes = {
|
||||
label: FormPropTypes.label.isRequired,
|
||||
dispatch: PropTypes.func,
|
||||
};
|
||||
|
||||
export class SimpleInput extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
|
@ -94,6 +105,26 @@ export class SimpleInput extends ImmutablePureComponent {
|
|||
|
||||
}
|
||||
|
||||
export class SimpleTextarea extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
label: FormPropTypes.label,
|
||||
hint: PropTypes.node,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { hint, ...props } = this.props;
|
||||
const Input = this.props.label ? LabelTextarea : 'textarea';
|
||||
|
||||
return (
|
||||
<InputContainer {...this.props}>
|
||||
<Input {...props} />
|
||||
</InputContainer>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class SimpleForm extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
SimpleForm,
|
||||
SimpleInput,
|
||||
TextInput,
|
||||
SimpleTextarea,
|
||||
Checkbox,
|
||||
} from 'soapbox/features/forms';
|
||||
import { register } from 'soapbox/actions/auth';
|
||||
|
@ -136,6 +137,16 @@ class RegistrationForm extends ImmutablePureComponent {
|
|||
onChange={this.onInputChange}
|
||||
required
|
||||
/>
|
||||
{instance.get('approval_required') &&
|
||||
<SimpleTextarea
|
||||
label={<FormattedMessage id='registration.reason' defaultMessage='Why do you want to join?' />}
|
||||
hint={<FormattedMessage id='registration.reason_hint' defaultMessage='This will help us review your application' />}
|
||||
name='reason'
|
||||
maxLength={500}
|
||||
autoComplete='off'
|
||||
onChange={this.onInputChange}
|
||||
required
|
||||
/>}
|
||||
</div>
|
||||
<CaptchaField
|
||||
onFetch={this.onFetchCaptcha}
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { makeGetAccount } from '../../selectors';
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import UserPanel from '../ui/components/user_panel';
|
||||
import ActionButton from '../ui/components/action_button';
|
||||
import { isAdmin, isModerator } from 'soapbox/utils/accounts';
|
||||
import Badge from 'soapbox/components/badge';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, { accountId }) => {
|
||||
return {
|
||||
account: getAccount(state, accountId),
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
class ProfileHoverCardContainer extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
visible: PropTypes.bool,
|
||||
accountId: PropTypes.string,
|
||||
account: ImmutablePropTypes.map,
|
||||
intl: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
visible: true,
|
||||
}
|
||||
|
||||
getBadges = () => {
|
||||
const { account } = this.props;
|
||||
let badges = [];
|
||||
if (isAdmin(account)) badges.push(<Badge key='admin' slug='admin' title='Admin' />);
|
||||
if (isModerator(account)) badges.push(<Badge key='moderator' slug='moderator' title='Moderator' />);
|
||||
if (account.getIn(['patron', 'is_patron'])) badges.push(<Badge key='patron' slug='patron' title='Patron' />);
|
||||
return badges;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { visible, accountId, account } = this.props;
|
||||
if (!accountId) return null;
|
||||
const accountBio = { __html: account.get('note_emojified') };
|
||||
const followedBy = account.getIn(['relationship', 'followed_by']);
|
||||
const badges = this.getBadges();
|
||||
|
||||
return (
|
||||
<div className={classNames('profile-hover-card', { 'profile-hover-card--visible': visible })}>
|
||||
<div className='profile-hover-card__container'>
|
||||
{followedBy &&
|
||||
<span className='relationship-tag'>
|
||||
<FormattedMessage id='account.follows_you' defaultMessage='Follows you' />
|
||||
</span>}
|
||||
<div className='profile-hover-card__action-button'><ActionButton account={account} /></div>
|
||||
<UserPanel className='profile-hover-card__user' accountId={accountId} />
|
||||
{badges.length > 0 &&
|
||||
<div className='profile-hover-card__badges'>
|
||||
{badges}
|
||||
</div>}
|
||||
{account.getIn(['source', 'note'], '').length > 0 &&
|
||||
<div className='profile-hover-card__bio' dangerouslySetInnerHTML={accountBio} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
|
@ -16,6 +16,9 @@ import classNames from 'classnames';
|
|||
import Icon from 'soapbox/components/icon';
|
||||
import PollContainer from 'soapbox/containers/poll_container';
|
||||
import { StatusInteractionBar } from './status_interaction_bar';
|
||||
import ProfileHoverCardContainer from 'soapbox/features/profile_hover_card/profile_hover_card_container';
|
||||
import { isMobile } from 'soapbox/is_mobile';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
export default class DetailedStatus extends ImmutablePureComponent {
|
||||
|
||||
|
@ -38,6 +41,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
|
||||
state = {
|
||||
height: null,
|
||||
profileCardVisible: false,
|
||||
};
|
||||
|
||||
handleOpenVideo = (media, startTime) => {
|
||||
|
@ -81,16 +85,31 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
window.open(href, 'soapbox-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
|
||||
}
|
||||
|
||||
showProfileCard = debounce(() => {
|
||||
this.setState({ profileCardVisible: true });
|
||||
}, 1200);
|
||||
|
||||
handleProfileHover = e => {
|
||||
if (!isMobile(window.innerWidth)) this.showProfileCard();
|
||||
}
|
||||
|
||||
handleProfileLeave = e => {
|
||||
this.showProfileCard.cancel();
|
||||
this.setState({ profileCardVisible: false });
|
||||
}
|
||||
|
||||
render() {
|
||||
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
|
||||
const outerStyle = { boxSizing: 'border-box' };
|
||||
const { compact } = this.props;
|
||||
const { profileCardVisible } = this.state;
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let media = '';
|
||||
let poll = '';
|
||||
let statusTypeIcon = '';
|
||||
|
||||
if (this.props.measureHeight) {
|
||||
|
@ -98,8 +117,9 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
if (status.get('poll')) {
|
||||
media = <PollContainer pollId={status.get('poll')} />;
|
||||
} else if (status.get('media_attachments').size > 0) {
|
||||
poll = <PollContainer pollId={status.get('poll')} />;
|
||||
}
|
||||
if (status.get('media_attachments').size > 0) {
|
||||
if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
const video = status.getIn(['media_attachments', 0]);
|
||||
|
||||
|
@ -158,10 +178,19 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
return (
|
||||
<div style={outerStyle}>
|
||||
<div ref={this.setRef} className={classNames('detailed-status', { compact })}>
|
||||
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} className='detailed-status__display-name'>
|
||||
<div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={48} /></div>
|
||||
<DisplayName account={status.get('account')} />
|
||||
</NavLink>
|
||||
<div className='detailed-status__profile' onMouseEnter={this.handleProfileHover} onMouseLeave={this.handleProfileLeave}>
|
||||
<div className='detailed-status__display-name'>
|
||||
<NavLink to={`/@${status.getIn(['account', 'acct'])}`}>
|
||||
<div className='detailed-status__display-avatar'>
|
||||
<Avatar account={status.get('account')} size={48} />
|
||||
</div>
|
||||
</NavLink>
|
||||
<DisplayName account={status.get('account')}>
|
||||
<NavLink to={`/@${status.getIn(['account', 'acct'])}`} title={status.getIn(['account', 'acct'])} className='floating-link' />
|
||||
</DisplayName>
|
||||
</div>
|
||||
<ProfileHoverCardContainer accountId={status.getIn(['account', 'id'])} visible={!isMobile(window.innerWidth) && profileCardVisible} />
|
||||
</div>
|
||||
|
||||
{status.get('group') && (
|
||||
<div className='status__meta'>
|
||||
|
@ -172,6 +201,7 @@ export default class DetailedStatus extends ImmutablePureComponent {
|
|||
<StatusContent status={status} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} />
|
||||
|
||||
{media}
|
||||
{poll}
|
||||
|
||||
<div className='detailed-status__meta'>
|
||||
<StatusInteractionBar status={status} />
|
||||
|
|
98
app/soapbox/features/ui/components/action_button.js
Normal file
98
app/soapbox/features/ui/components/action_button.js
Normal file
|
@ -0,0 +1,98 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import Button from 'soapbox/components/button';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
followAccount,
|
||||
unfollowAccount,
|
||||
blockAccount,
|
||||
unblockAccount,
|
||||
} from 'soapbox/actions/accounts';
|
||||
|
||||
const messages = defineMessages({
|
||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
|
||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const me = state.get('me');
|
||||
return {
|
||||
me,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
onFollow(account) {
|
||||
if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
|
||||
dispatch(unfollowAccount(account.get('id')));
|
||||
} else {
|
||||
dispatch(followAccount(account.get('id')));
|
||||
}
|
||||
},
|
||||
|
||||
onBlock(account) {
|
||||
if (account.getIn(['relationship', 'blocking'])) {
|
||||
dispatch(unblockAccount(account.get('id')));
|
||||
} else {
|
||||
dispatch(blockAccount(account.get('id')));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
class ActionButton extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
onFollow: PropTypes.func.isRequired,
|
||||
onBlock: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('resize', this.handleResize, { passive: true });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
}
|
||||
|
||||
handleFollow = () => {
|
||||
this.props.onFollow(this.props.account);
|
||||
}
|
||||
|
||||
handleBlock = () => {
|
||||
this.props.onBlock(this.props.account);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { account, intl, me } = this.props;
|
||||
let actionBtn = null;
|
||||
|
||||
if (!account || !me) return actionBtn;
|
||||
|
||||
if (me !== account.get('id')) {
|
||||
if (!account.get('relationship')) { // Wait until the relationship is loaded
|
||||
//
|
||||
} else if (account.getIn(['relationship', 'requested'])) {
|
||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.requested)} onClick={this.handleFollow} />;
|
||||
} 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, { name: account.get('username') })} onClick={this.handleBlock} />;
|
||||
}
|
||||
} else {
|
||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} to='/settings/profile' />;
|
||||
}
|
||||
return actionBtn;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,25 +1,65 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { connect } from 'react-redux';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import IconButton from 'soapbox/components/icon_button';
|
||||
import { changeSetting, getSettings } from 'soapbox/actions/settings';
|
||||
|
||||
export default
|
||||
|
||||
const messages = defineMessages({
|
||||
collapse: { id: 'explanation_box.collapse', defaultMessage: 'Collapse explanation box' },
|
||||
expand: { id: 'explanation_box.expand', defaultMessage: 'Expand explanation box' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
settings: getSettings(state),
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
toggleExplanationBox(setting) {
|
||||
dispatch(changeSetting(['explanationBox'], setting));
|
||||
},
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
class ExplanationBox extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
|
||||
explanation: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
|
||||
dismissable: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
settings: ImmutablePropTypes.map.isRequired,
|
||||
toggleExplanationBox: PropTypes.func,
|
||||
};
|
||||
|
||||
handleToggleExplanationBox = () => {
|
||||
this.props.toggleExplanationBox(this.props.settings.get('explanationBox') === true ? false : true);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { title, explanation, dismissable } = this.props;
|
||||
const { title, explanation, dismissable, settings, intl } = this.props;
|
||||
|
||||
return (
|
||||
<div className='explanation-box'>
|
||||
{title && <div className='explanation-box__title'>{title}</div>}
|
||||
<div className='explanation-box__explanation'>
|
||||
{explanation}
|
||||
{dismissable && <span className='explanation-box__dismiss'>Dismiss</span>}
|
||||
</div>
|
||||
{title && <div className='explanation-box__title'>{title}
|
||||
<IconButton
|
||||
className='explanation_box__toggle' size={20}
|
||||
title={settings.get('explanationBox') ? intl.formatMessage(messages.collapse) : intl.formatMessage(messages.expand)}
|
||||
icon={settings.get('explanationBox') ? 'angle-down' : 'angle-up'}
|
||||
onClick={this.handleToggleExplanationBox}
|
||||
/>
|
||||
</div>}
|
||||
{settings.get('explanationBox') &&
|
||||
<div className='explanation-box__explanation'>
|
||||
{explanation}
|
||||
{dismissable && <span className='explanation-box__dismiss'>Dismiss</span>}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -84,17 +84,17 @@ class UserPanel extends ImmutablePureComponent {
|
|||
|
||||
};
|
||||
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const me = state.get('me');
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
return {
|
||||
account: getAccount(state, me),
|
||||
};
|
||||
const mapStateToProps = (state, { accountId }) => ({
|
||||
account: getAccount(state, accountId),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
export default injectIntl(
|
||||
connect(mapStateToProps, null, null, {
|
||||
connect(makeMapStateToProps, null, null, {
|
||||
forwardRef: true,
|
||||
})(UserPanel));
|
||||
|
|
|
@ -64,6 +64,7 @@ import {
|
|||
// GroupTimeline,
|
||||
ListTimeline,
|
||||
Lists,
|
||||
Bookmarks,
|
||||
// GroupMembers,
|
||||
// GroupRemovedAccounts,
|
||||
// GroupCreate,
|
||||
|
@ -228,6 +229,7 @@ class SwitchingColumnsArea extends React.PureComponent {
|
|||
|
||||
<WrappedRoute path='/lists' layout={LAYOUT.DEFAULT} component={Lists} content={children} />
|
||||
<WrappedRoute path='/list/:id' page={HomePage} component={ListTimeline} content={children} />
|
||||
<WrappedRoute path='/bookmarks' layout={LAYOUT.DEFAULT} component={Bookmarks} content={children} />
|
||||
|
||||
<WrappedRoute path='/notifications' layout={LAYOUT.DEFAULT} component={Notifications} content={children} />
|
||||
|
||||
|
|
|
@ -62,6 +62,10 @@ export function Lists() {
|
|||
return import(/* webpackChunkName: "features/lists" */'../../lists');
|
||||
}
|
||||
|
||||
export function Bookmarks() {
|
||||
return import(/* webpackChunkName: "features/bookmarks" */'../../bookmarks');
|
||||
}
|
||||
|
||||
export function Status() {
|
||||
return import(/* webpackChunkName: "features/status" */'../../status');
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import { getFeatures } from 'soapbox/utils/features';
|
|||
const mapStateToProps = state => {
|
||||
const me = state.get('me');
|
||||
return {
|
||||
me,
|
||||
account: state.getIn(['accounts', me]),
|
||||
hasPatron: state.getIn(['soapbox', 'extensions', 'patron', 'enabled']),
|
||||
features: getFeatures(state.get('instance')),
|
||||
|
@ -30,7 +31,7 @@ class HomePage extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { children, account, hasPatron, features } = this.props;
|
||||
const { me, children, account, hasPatron, features } = this.props;
|
||||
|
||||
return (
|
||||
<div className='page'>
|
||||
|
@ -39,7 +40,7 @@ class HomePage extends ImmutablePureComponent {
|
|||
|
||||
<div className='columns-area__panels__pane columns-area__panels__pane--left'>
|
||||
<div className='columns-area__panels__pane__inner'>
|
||||
<UserPanel />
|
||||
<UserPanel accountId={me} />
|
||||
{hasPatron && <FundingPanel />}
|
||||
<PromoPanel />
|
||||
<LinkFooter />
|
||||
|
|
|
@ -50,18 +50,18 @@ describe('alerts reducer', () => {
|
|||
// });
|
||||
|
||||
it('should handle ALERT_CLEAR', () => {
|
||||
const state = ImmutableList([
|
||||
{
|
||||
key: 0,
|
||||
message: 'message_1',
|
||||
title: 'title_1',
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
message: 'message_2',
|
||||
title: 'title_2',
|
||||
},
|
||||
]);
|
||||
const state = ImmutableList([
|
||||
{
|
||||
key: 0,
|
||||
message: 'message_1',
|
||||
title: 'title_1',
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
message: 'message_2',
|
||||
title: 'title_2',
|
||||
},
|
||||
]);
|
||||
const action = {
|
||||
type: actions.ALERT_CLEAR,
|
||||
};
|
||||
|
|
|
@ -32,6 +32,7 @@ describe('compose reducer', () => {
|
|||
default_sensitive: false,
|
||||
idempotencyKey: null,
|
||||
tagHistory: [],
|
||||
content_type: 'text/markdown',
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -168,8 +169,18 @@ describe('compose reducer', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should handle COMPOSE_SENSITIVITY_CHANGE on Mark Sensitive click, don\'t toggle if spoiler active', () => {
|
||||
const state = ImmutableMap({ spoiler: true, sensitive: true, idempotencyKey: null });
|
||||
const action = {
|
||||
type: actions.COMPOSE_SENSITIVITY_CHANGE,
|
||||
};
|
||||
expect(reducer(state, action).toJS()).toMatchObject({
|
||||
sensitive: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle COMPOSE_SENSITIVITY_CHANGE on Mark Sensitive click, toggle if spoiler inactive', () => {
|
||||
const state = ImmutableMap({ sensitive: true });
|
||||
const state = ImmutableMap({ spoiler: false, sensitive: true });
|
||||
const action = {
|
||||
type: actions.COMPOSE_SENSITIVITY_CHANGE,
|
||||
};
|
||||
|
@ -777,4 +788,11 @@ describe('compose reducer', () => {
|
|||
// });
|
||||
// });
|
||||
|
||||
it('sets the post content-type', () => {
|
||||
const action = {
|
||||
type: actions.COMPOSE_TYPE_CHANGE,
|
||||
value: 'text/plain',
|
||||
};
|
||||
expect(reducer(undefined, action).toJS()).toMatchObject({ content_type: 'text/plain' });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -206,16 +206,16 @@ describe('notifications reducer', () => {
|
|||
};
|
||||
expect(reducer(state, action)).toEqual(ImmutableMap({
|
||||
items: ImmutableList([
|
||||
ImmutableMap({
|
||||
id: '10743',
|
||||
type: 'favourite',
|
||||
account: '9v5c6xSEgAi3Zu1Lv6',
|
||||
created_at: '2020-06-10T02:51:05.000Z',
|
||||
status: '9vvNxoo5EFbbnfdXQu',
|
||||
emoji: undefined,
|
||||
is_seen: true,
|
||||
}),
|
||||
]),
|
||||
ImmutableMap({
|
||||
id: '10743',
|
||||
type: 'favourite',
|
||||
account: '9v5c6xSEgAi3Zu1Lv6',
|
||||
created_at: '2020-06-10T02:51:05.000Z',
|
||||
status: '9vvNxoo5EFbbnfdXQu',
|
||||
emoji: undefined,
|
||||
is_seen: true,
|
||||
}),
|
||||
]),
|
||||
top: false,
|
||||
unread: 2,
|
||||
}));
|
||||
|
|
|
@ -9,6 +9,11 @@ describe('status_lists reducer', () => {
|
|||
loaded: false,
|
||||
items: ImmutableList(),
|
||||
}),
|
||||
bookmarks: ImmutableMap({
|
||||
next: null,
|
||||
loaded: false,
|
||||
items: ImmutableList(),
|
||||
}),
|
||||
pins: ImmutableMap({
|
||||
next: null,
|
||||
loaded: false,
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
COMPOSE_TAG_HISTORY_UPDATE,
|
||||
COMPOSE_SENSITIVITY_CHANGE,
|
||||
COMPOSE_SPOILERNESS_CHANGE,
|
||||
COMPOSE_TYPE_CHANGE,
|
||||
COMPOSE_SPOILER_TEXT_CHANGE,
|
||||
COMPOSE_VISIBILITY_CHANGE,
|
||||
COMPOSE_COMPOSING_CHANGE,
|
||||
|
@ -50,6 +51,7 @@ const initialState = ImmutableMap({
|
|||
sensitive: false,
|
||||
spoiler: false,
|
||||
spoiler_text: '',
|
||||
content_type: 'text/markdown',
|
||||
privacy: null,
|
||||
text: '',
|
||||
focusDate: null,
|
||||
|
@ -94,6 +96,7 @@ function clearAll(state) {
|
|||
map.set('text', '');
|
||||
map.set('spoiler', false);
|
||||
map.set('spoiler_text', '');
|
||||
map.set('content_type', 'text/markdown');
|
||||
map.set('is_submitting', false);
|
||||
map.set('is_changing_upload', false);
|
||||
map.set('in_reply_to', null);
|
||||
|
@ -114,7 +117,7 @@ function appendMedia(state, media) {
|
|||
map.set('resetFileKey', Math.floor((Math.random() * 0x10000)));
|
||||
map.set('idempotencyKey', uuid());
|
||||
|
||||
if (prevSize === 0 && state.get('default_sensitive')) {
|
||||
if (prevSize === 0 && (state.get('default_sensitive') || state.get('spoiler'))) {
|
||||
map.set('sensitive', true);
|
||||
}
|
||||
});
|
||||
|
@ -211,8 +214,15 @@ export default function compose(state = initialState, action) {
|
|||
.set('is_composing', false);
|
||||
case COMPOSE_SENSITIVITY_CHANGE:
|
||||
return state.withMutations(map => {
|
||||
map.set('sensitive', !state.get('sensitive'));
|
||||
if (!state.get('spoiler')) {
|
||||
map.set('sensitive', !state.get('sensitive'));
|
||||
}
|
||||
|
||||
map.set('idempotencyKey', uuid());
|
||||
});
|
||||
case COMPOSE_TYPE_CHANGE:
|
||||
return state.withMutations(map => {
|
||||
map.set('content_type', action.value);
|
||||
map.set('idempotencyKey', uuid());
|
||||
});
|
||||
case COMPOSE_SPOILERNESS_CHANGE:
|
||||
|
@ -220,6 +230,10 @@ export default function compose(state = initialState, action) {
|
|||
map.set('spoiler_text', '');
|
||||
map.set('spoiler', !state.get('spoiler'));
|
||||
map.set('idempotencyKey', uuid());
|
||||
|
||||
if (!state.get('sensitive') && state.get('media_attachments').size >= 1) {
|
||||
map.set('sensitive', true);
|
||||
}
|
||||
});
|
||||
case COMPOSE_SPOILER_TEXT_CHANGE:
|
||||
return state
|
||||
|
@ -243,6 +257,7 @@ export default function compose(state = initialState, action) {
|
|||
map.set('focusDate', new Date());
|
||||
map.set('caretPosition', null);
|
||||
map.set('idempotencyKey', uuid());
|
||||
map.set('content_type', 'text/markdown');
|
||||
|
||||
if (action.status.get('spoiler_text', '').length > 0) {
|
||||
map.set('spoiler', true);
|
||||
|
@ -326,6 +341,7 @@ export default function compose(state = initialState, action) {
|
|||
map.set('focusDate', new Date());
|
||||
map.set('caretPosition', null);
|
||||
map.set('idempotencyKey', uuid());
|
||||
map.set('content_type', 'text/markdown');
|
||||
|
||||
if (action.status.get('spoiler_text').length > 0) {
|
||||
map.set('spoiler', true);
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
DOMAIN_UNBLOCK_SUCCESS,
|
||||
} from '../actions/domain_blocks';
|
||||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||
import { get } from 'lodash';
|
||||
|
||||
const normalizeRelationship = (state, relationship) => state.set(relationship.id, fromJS(relationship));
|
||||
|
||||
|
@ -42,8 +43,10 @@ const setDomainBlocking = (state, accounts, blocking) => {
|
|||
};
|
||||
|
||||
const importPleromaAccount = (state, account) => {
|
||||
if (!account.pleroma) return state;
|
||||
return normalizeRelationship(state, account.pleroma.relationship);
|
||||
const relationship = get(account, ['pleroma', 'relationship'], {});
|
||||
if (relationship.id && relationship !== {})
|
||||
return normalizeRelationship(state, relationship);
|
||||
return state;
|
||||
};
|
||||
|
||||
const importPleromaAccounts = (state, accounts) => {
|
||||
|
|
|
@ -6,6 +6,14 @@ import {
|
|||
FAVOURITED_STATUSES_EXPAND_SUCCESS,
|
||||
FAVOURITED_STATUSES_EXPAND_FAIL,
|
||||
} from '../actions/favourites';
|
||||
import {
|
||||
BOOKMARKED_STATUSES_FETCH_REQUEST,
|
||||
BOOKMARKED_STATUSES_FETCH_SUCCESS,
|
||||
BOOKMARKED_STATUSES_FETCH_FAIL,
|
||||
BOOKMARKED_STATUSES_EXPAND_REQUEST,
|
||||
BOOKMARKED_STATUSES_EXPAND_SUCCESS,
|
||||
BOOKMARKED_STATUSES_EXPAND_FAIL,
|
||||
} from '../actions/bookmarks';
|
||||
import {
|
||||
PINNED_STATUSES_FETCH_SUCCESS,
|
||||
} from '../actions/pin_statuses';
|
||||
|
@ -13,6 +21,8 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
|||
import {
|
||||
FAVOURITE_SUCCESS,
|
||||
UNFAVOURITE_SUCCESS,
|
||||
BOOKMARK_SUCCESS,
|
||||
UNBOOKMARK_SUCCESS,
|
||||
PIN_SUCCESS,
|
||||
UNPIN_SUCCESS,
|
||||
} from '../actions/interactions';
|
||||
|
@ -23,6 +33,11 @@ const initialState = ImmutableMap({
|
|||
loaded: false,
|
||||
items: ImmutableList(),
|
||||
}),
|
||||
bookmarks: ImmutableMap({
|
||||
next: null,
|
||||
loaded: false,
|
||||
items: ImmutableList(),
|
||||
}),
|
||||
pins: ImmutableMap({
|
||||
next: null,
|
||||
loaded: false,
|
||||
|
@ -71,10 +86,24 @@ export default function statusLists(state = initialState, action) {
|
|||
return normalizeList(state, 'favourites', action.statuses, action.next);
|
||||
case FAVOURITED_STATUSES_EXPAND_SUCCESS:
|
||||
return appendToList(state, 'favourites', action.statuses, action.next);
|
||||
case BOOKMARKED_STATUSES_FETCH_REQUEST:
|
||||
case BOOKMARKED_STATUSES_EXPAND_REQUEST:
|
||||
return state.setIn(['bookmarks', 'isLoading'], true);
|
||||
case BOOKMARKED_STATUSES_FETCH_FAIL:
|
||||
case BOOKMARKED_STATUSES_EXPAND_FAIL:
|
||||
return state.setIn(['bookmarks', 'isLoading'], false);
|
||||
case BOOKMARKED_STATUSES_FETCH_SUCCESS:
|
||||
return normalizeList(state, 'bookmarks', action.statuses, action.next);
|
||||
case BOOKMARKED_STATUSES_EXPAND_SUCCESS:
|
||||
return appendToList(state, 'bookmarks', action.statuses, action.next);
|
||||
case FAVOURITE_SUCCESS:
|
||||
return prependOneToList(state, 'favourites', action.status);
|
||||
case UNFAVOURITE_SUCCESS:
|
||||
return removeOneFromList(state, 'favourites', action.status);
|
||||
case BOOKMARK_SUCCESS:
|
||||
return prependOneToList(state, 'bookmarks', action.status);
|
||||
case UNBOOKMARK_SUCCESS:
|
||||
return removeOneFromList(state, 'bookmarks', action.status);
|
||||
case PINNED_STATUSES_FETCH_SUCCESS:
|
||||
return normalizeList(state, 'pins', action.statuses, action.next);
|
||||
case PIN_SUCCESS:
|
||||
|
|
|
@ -71,3 +71,4 @@
|
|||
@import 'components/error-boundary';
|
||||
@import 'components/video-player';
|
||||
@import 'components/audio-player';
|
||||
@import 'components/profile_hover_card';
|
||||
|
|
|
@ -170,7 +170,10 @@ body {
|
|||
&__title {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
&__explanation {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
&__dismiss {
|
||||
|
@ -215,3 +218,14 @@ noscript {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.floating-link {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
background: var(--accent-color--med);
|
||||
@media screen and (max-width: 895px) {height: 225px;}
|
||||
&--none {height: 125px;}
|
||||
|
||||
img {
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
|
@ -30,6 +31,10 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.still-image {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.still-image--play-on-hover::before {
|
||||
content: 'GIF';
|
||||
position: absolute;
|
||||
|
|
|
@ -173,11 +173,11 @@
|
|||
white-space: nowrap;
|
||||
overlow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
background: hsl( var(--brand-color_h), var(--brand-color_s), 20% );
|
||||
background: hsl(var(--brand-color_h), var(--brand-color_s), 20%);
|
||||
padding: 5px;
|
||||
|
||||
&__label {
|
||||
color: white;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
button {
|
||||
|
|
|
@ -212,6 +212,7 @@
|
|||
font-size: 16px;
|
||||
line-height: inherit;
|
||||
border: 0;
|
||||
border-radius: 10px 10px 0 0;
|
||||
text-align: unset;
|
||||
padding: 15px;
|
||||
margin: 0;
|
||||
|
@ -260,7 +261,7 @@
|
|||
position: absolute;
|
||||
right: 0;
|
||||
top: -49px;
|
||||
|
||||
|
||||
@media screen and (max-width: $nav-breakpoint-2) {
|
||||
top: -35px;
|
||||
font-size: 14px;
|
||||
|
|
|
@ -298,6 +298,7 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 0 0 5px 5px;
|
||||
|
||||
.compose-form__buttons {
|
||||
display: flex;
|
||||
|
|
|
@ -62,6 +62,7 @@
|
|||
border-bottom: 1px solid var(--brand-color--faint);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-radius: 0 0 10px 10px;
|
||||
}
|
||||
|
||||
.detailed-status__link {
|
||||
|
|
|
@ -41,6 +41,7 @@ a.account__display-name {
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.display-name__html {
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
.column,
|
||||
.drawer {
|
||||
flex: 1 1 100%;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.drawer__pager {
|
||||
|
|
131
app/styles/components/profile_hover_card.scss
Normal file
131
app/styles/components/profile_hover_card.scss
Normal file
|
@ -0,0 +1,131 @@
|
|||
.display-name__account {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.display-name .profile-hover-card {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.profile-hover-card {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition-property: opacity;
|
||||
transition-duration: 0.2s;
|
||||
width: 320px;
|
||||
z-index: 998;
|
||||
left: -10px;
|
||||
padding: 20px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&--visible {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
@media(min-width: 750px) {
|
||||
left: -100px;
|
||||
}
|
||||
|
||||
.profile-hover-card__container {
|
||||
@include standard-panel;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.profile-hover-card__action-button {
|
||||
z-index: 999;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
top: 40px;
|
||||
}
|
||||
|
||||
.user-panel {
|
||||
box-shadow: none;
|
||||
width: auto;
|
||||
|
||||
.user-panel-stats-item a strong {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&__header {
|
||||
height: 96px;
|
||||
}
|
||||
|
||||
.user-panel-stats-item {
|
||||
margin-right: 10px;
|
||||
|
||||
&__label,
|
||||
&__value {
|
||||
display: inline;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
&__value {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.relationship-tag {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.profile-hover-card__badges {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 110px;
|
||||
left: 120px;
|
||||
|
||||
.badge {
|
||||
padding: 2px 4px;
|
||||
margin-right: 5px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-hover-card__bio {
|
||||
margin: 0 20px 20px;
|
||||
max-height: 4em;
|
||||
|
||||
a {
|
||||
color: var(--highlight-text-color);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: linear-gradient(0deg, var(--foreground-color) 0%, var(--foreground-color), 80%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.detailed-status {
|
||||
.profile-hover-card {
|
||||
top: 0;
|
||||
left: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Prevent floating avatars from intercepting with current card */
|
||||
.status,
|
||||
.detailed-status {
|
||||
.floating-link {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover .floating-link {
|
||||
display: block;
|
||||
}
|
||||
}
|
|
@ -1,3 +1,61 @@
|
|||
.status__content {
|
||||
p,
|
||||
li {
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
p,
|
||||
li {
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
ol,
|
||||
blockquote {
|
||||
margin-bottom: 20px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: disc inside none;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: decimal inside none;
|
||||
}
|
||||
|
||||
blockquote p {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
p > code {
|
||||
padding: 2px 4px;
|
||||
background-color: var(--background-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Code block */
|
||||
pre {
|
||||
line-height: 1.6em;
|
||||
overflow-x: auto;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 20px;
|
||||
word-break: break-all;
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.status__content--with-action {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -152,7 +210,6 @@
|
|||
.status__info .status__display-name {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
padding-right: 25px;
|
||||
}
|
||||
|
||||
.status__info {
|
||||
|
@ -160,6 +217,16 @@
|
|||
z-index: 4;
|
||||
}
|
||||
|
||||
.status__profile,
|
||||
.detailed-status__profile {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.status__profile {
|
||||
padding-right: 25px;
|
||||
}
|
||||
|
||||
.status-check-box {
|
||||
border-bottom: 1px solid var(--background-color);
|
||||
display: flex;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
.setting-toggle {
|
||||
|
||||
&__label {
|
||||
margin-bottom: 0px;
|
||||
margin-bottom: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,8 @@
|
|||
|
||||
&-track-check,
|
||||
&-track-x {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 15px;
|
||||
color: #fff;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,14 @@
|
|||
display: flex;
|
||||
width: 265px;
|
||||
flex-direction: column;
|
||||
overflow-y: hidden;
|
||||
|
||||
&,
|
||||
.user-panel__account__name,
|
||||
.user-panel__account__username {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: block;
|
||||
|
|
|
@ -109,6 +109,7 @@
|
|||
input {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: var(--primary-text-color);
|
||||
padding: 7px 9px;
|
||||
font-family: inherit;
|
||||
display: block;
|
||||
|
|
|
@ -188,6 +188,7 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
border-radius: 0 0 10px 10px;
|
||||
|
||||
& > div {
|
||||
width: 100%;
|
||||
|
|
Loading…
Reference in a new issue