diff options
Diffstat (limited to 'app/javascript/flavours')
58 files changed, 1681 insertions, 139 deletions
diff --git a/app/javascript/flavours/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js index 912a3d179..a4eef2fd2 100644 --- a/app/javascript/flavours/glitch/actions/accounts.js +++ b/app/javascript/flavours/glitch/actions/accounts.js @@ -274,11 +274,11 @@ export function unblockAccountFail(error) { }; -export function muteAccount(id, notifications, duration=0) { +export function muteAccount(id, notifications, timelinesOnly, duration=0) { return (dispatch, getState) => { dispatch(muteAccountRequest(id)); - api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications, duration }).then(response => { + api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications, timelinesOnly, duration }).then(response => { // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers dispatch(muteAccountSuccess(response.data, getState().get('statuses'))); }).catch(error => { diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index f83738093..8c126c6e2 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -147,16 +147,16 @@ export function submitCompose(routerHistory) { let media = getState().getIn(['compose', 'media_attachments']); const spoilers = getState().getIn(['compose', 'spoiler']) || getState().getIn(['local_settings', 'always_show_spoilers_field']); let spoilerText = spoilers ? getState().getIn(['compose', 'spoiler_text'], '') : ''; + const id = getState().getIn(['compose', 'id'], null); + const submit_url = id ? `/api/v1/statuses/${id}` : '/api/v1/statuses'; + const submit_action = (res, body, config) => id ? api(getState).put(res, body, config) : api(getState).post(res, body, config); if ((!status || !status.length) && media.size === 0) { return; } dispatch(submitComposeRequest()); - if (getState().getIn(['compose', 'advanced_options', 'do_not_federate'])) { - status = status + ' 👁️'; - } - api(getState).post('/api/v1/statuses', { + submit_action(submit_url, { status, content_type: getState().getIn(['compose', 'content_type']), in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), @@ -165,6 +165,7 @@ export function submitCompose(routerHistory) { spoiler_text: spoilerText, visibility: getState().getIn(['compose', 'privacy']), poll: getState().getIn(['compose', 'poll'], null), + local_only: getState().getIn(['compose', 'advanced_options', 'do_not_federate']), }, { headers: { 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), diff --git a/app/javascript/flavours/glitch/actions/importer/normalizer.js b/app/javascript/flavours/glitch/actions/importer/normalizer.js index 05955963c..729c8d700 100644 --- a/app/javascript/flavours/glitch/actions/importer/normalizer.js +++ b/app/javascript/flavours/glitch/actions/importer/normalizer.js @@ -12,7 +12,7 @@ const makeEmojiMap = record => record.emojis.reduce((obj, emoji) => { export function searchTextFromRawStatus (status) { const spoilerText = status.spoiler_text || ''; - const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n'); + const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).concat(status.tags ? status.tags.map(tag => tag.name) : []).join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n'); return domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; } @@ -53,11 +53,15 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.poll = status.poll.id; } + const oldUpdatedAt = normalOldStatus ? normalOldStatus.updated_at || normalOldStatus.created_at : null; + const newUpdatedAt = normalStatus ? normalStatus.updated_at || normalStatus.created_at : null; + // Only calculate these values when status first encountered // Otherwise keep the ones already in the reducer - if (normalOldStatus) { + if (normalOldStatus && oldUpdatedAt === newUpdatedAt) { normalStatus.search_index = normalOldStatus.get('search_index'); normalStatus.contentHtml = normalOldStatus.get('contentHtml'); + normalStatus.articleHtml = normalOldStatus.get('articleHtml'); normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml'); } else { const spoilerText = normalStatus.spoiler_text || ''; @@ -66,6 +70,7 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); + normalStatus.articleHtml = normalStatus.article_content ? emojify(normalStatus.article_content, emojiMap) : normalStatus.contentHtml; normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap); } diff --git a/app/javascript/flavours/glitch/actions/mutes.js b/app/javascript/flavours/glitch/actions/mutes.js index 2bacfadb7..b85cc7863 100644 --- a/app/javascript/flavours/glitch/actions/mutes.js +++ b/app/javascript/flavours/glitch/actions/mutes.js @@ -14,6 +14,7 @@ export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL'; export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL'; export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS'; export const MUTES_CHANGE_DURATION = 'MUTES_CHANGE_DURATION'; +export const MUTES_TOGGLE_TIMELINES_ONLY = 'MUTES_TOGGLE_TIMELINES_ONLY'; export function fetchMutes() { return (dispatch, getState) => { @@ -114,3 +115,9 @@ export function changeMuteDuration(duration) { }); }; } + +export function toggleTimelinesOnly() { + return dispatch => { + dispatch({ type: MUTES_TOGGLE_TIMELINES_ONLY }); + }; +} diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index 7273191b2..091a4b435 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -13,7 +13,7 @@ import { defineMessages } from 'react-intl'; import { List as ImmutableList } from 'immutable'; import { unescapeHTML } from 'flavours/glitch/util/html'; import { getFiltersRegex } from 'flavours/glitch/selectors'; -import { usePendingItems as preferPendingItems } from 'flavours/glitch/util/initial_state'; +import { usePendingItems as preferPendingItems, webPushEnabled } from 'flavours/glitch/util/initial_state'; import compareId from 'flavours/glitch/util/compare_id'; import { searchTextFromRawStatus } from 'flavours/glitch/actions/importer/normalizer'; import { requestNotificationPermission } from 'flavours/glitch/util/notifications'; @@ -337,6 +337,11 @@ export function markNotificationsAsRead() { // Browser support export function setupBrowserNotifications() { return dispatch => { + if (!webPushEnabled) { + dispatch(setBrowserSupport(false)); + return; + } + dispatch(setBrowserSupport('Notification' in window)); if ('Notification' in window) { dispatch(setBrowserPermission(Notification.permission)); diff --git a/app/javascript/flavours/glitch/actions/push_notifications/registerer.js b/app/javascript/flavours/glitch/actions/push_notifications/registerer.js index 8fdb239f7..1f9ebe569 100644 --- a/app/javascript/flavours/glitch/actions/push_notifications/registerer.js +++ b/app/javascript/flavours/glitch/actions/push_notifications/registerer.js @@ -1,7 +1,9 @@ import api from 'flavours/glitch/util/api'; +import { webPushEnabled } from 'flavours/glitch/util/initial_state'; import { pushNotificationsSetting } from 'flavours/glitch/util/settings'; import { setBrowserSupport, setSubscription, clearSubscription } from './setter'; + // Taken from https://www.npmjs.com/package/web-push const urlBase64ToUint8Array = (base64String) => { const padding = '='.repeat((4 - base64String.length % 4) % 4); @@ -49,7 +51,7 @@ const sendSubscriptionToBackend = (getState, subscription, me) => { }; // Last one checks for payload support: https://web-push-book.gauntface.com/chapter-06/01-non-standards-browsers/#no-payload -const supportsPushNotifications = ('serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype); +const supportsPushNotifications = (webPushEnabled && 'serviceWorker' in navigator && 'PushManager' in window && 'getKey' in PushSubscription.prototype); export function register () { return (dispatch, getState) => { diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js index 4d2bda78b..018641fc7 100644 --- a/app/javascript/flavours/glitch/actions/statuses.js +++ b/app/javascript/flavours/glitch/actions/statuses.js @@ -12,6 +12,10 @@ export const STATUS_DELETE_REQUEST = 'STATUS_DELETE_REQUEST'; export const STATUS_DELETE_SUCCESS = 'STATUS_DELETE_SUCCESS'; export const STATUS_DELETE_FAIL = 'STATUS_DELETE_FAIL'; +export const STATUS_PUBLISH_REQUEST = 'STATUS_PUBLISH_REQUEST'; +export const STATUS_PUBLISH_SUCCESS = 'STATUS_PUBLISH_SUCCESS'; +export const STATUS_PUBLISH_FAIL = 'STATUS_PUBLISH_FAIL'; + export const CONTEXT_FETCH_REQUEST = 'CONTEXT_FETCH_REQUEST'; export const CONTEXT_FETCH_SUCCESS = 'CONTEXT_FETCH_SUCCESS'; export const CONTEXT_FETCH_FAIL = 'CONTEXT_FETCH_FAIL'; @@ -34,9 +38,9 @@ export function fetchStatusRequest(id, skipLoading) { }; }; -export function fetchStatus(id) { +export function fetchStatus(id, skipLoading = null) { return (dispatch, getState) => { - const skipLoading = getState().getIn(['statuses', id], null) !== null; + skipLoading = skipLoading === null ? getState().getIn(['statuses', id], null) !== null : skipLoading; dispatch(fetchContext(id)); @@ -55,6 +59,59 @@ export function fetchStatus(id) { }; }; +export function editStatus(status, routerHistory) { + return (dispatch, getState) => { + const id = status.get('id'); + + dispatch(fetchContext(id)); + dispatch(fetchStatusRequest(id, false)); + + api(getState).get(`/api/v1/statuses/${id}`, { params: { source: 1 } }).then(response => { + dispatch(importFetchedStatus(response.data)); + dispatch(fetchStatusSuccess(false)); + dispatch(redraft(status, response.data.text, response.data.content_type, true)); + ensureComposeIsVisible(getState, routerHistory); + }).catch(error => { + dispatch(fetchStatusFail(id, error, false)); + }); + }; +}; + +export function publishStatus(id) { + return (dispatch, getState) => { + dispatch(publishStatusRequest(id)); + + api(getState).post(`/api/v1/statuses/${id}/publish`).then(() => { + dispatch(publishStatusSuccess(id)); + dispatch(fetchStatus(id, false)); + }).catch(error => { + dispatch(publishStatusFail(id, error)); + }); + }; +}; + +export function publishStatusRequest(id) { + return { + type: STATUS_PUBLISH_REQUEST, + id: id, + }; +}; + +export function publishStatusSuccess(id) { + return { + type: STATUS_PUBLISH_SUCCESS, + id: id, + }; +}; + +export function publishStatusFail(id, error) { + return { + type: STATUS_PUBLISH_FAIL, + id: id, + error: error, + }; +}; + export function fetchStatusSuccess(skipLoading) { return { type: STATUS_FETCH_SUCCESS, @@ -72,12 +129,13 @@ export function fetchStatusFail(id, error, skipLoading) { }; }; -export function redraft(status, raw_text, content_type) { +export function redraft(status, raw_text, content_type, inplace = false) { return { type: REDRAFT, status, raw_text, content_type, + inplace, }; }; @@ -91,7 +149,7 @@ export function deleteStatus(id, routerHistory, withRedraft = false) { dispatch(deleteStatusRequest(id)); - api(getState).delete(`/api/v1/statuses/${id}`).then(response => { + api(getState).delete(`/api/v1/statuses/${id}`, { params: { redraft: withRedraft?1:0 } } ).then(response => { dispatch(deleteStatusSuccess(id)); dispatch(deleteFromTimelines(id)); @@ -172,12 +230,16 @@ export function fetchContextFail(id, error) { }; }; -export function muteStatus(id) { +export function muteStatus(id, hide = false) { return (dispatch, getState) => { dispatch(muteStatusRequest(id)); - api(getState).post(`/api/v1/statuses/${id}/mute`).then(() => { + api(getState).post(`/api/v1/statuses/${id}/mute`, { params: { hide: hide?1:0 } }).then(() => { dispatch(muteStatusSuccess(id)); + + if (hide) { + dispatch(deleteFromTimelines(id)); + } }).catch(error => { dispatch(muteStatusFail(id, error)); }); diff --git a/app/javascript/flavours/glitch/actions/streaming.js b/app/javascript/flavours/glitch/actions/streaming.js index 35db5dcc9..295896e55 100644 --- a/app/javascript/flavours/glitch/actions/streaming.js +++ b/app/javascript/flavours/glitch/actions/streaming.js @@ -18,6 +18,7 @@ import { } from './announcements'; import { fetchFilters } from './filters'; import { getLocale } from 'mastodon/locales'; +import { resetCompose } from 'flavours/glitch/actions/compose'; const { messages } = getLocale(); @@ -96,6 +97,10 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti case 'announcement.delete': dispatch(deleteAnnouncement(data.payload)); break; + case 'refresh': + dispatch(resetCompose()); + window.location.reload(); + break; } }, }; diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index b19666e62..bd79d64f5 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -133,7 +133,18 @@ export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => ex export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote, allowLocalOnly } = {}, done = noOp) => expandTimeline(`public${onlyRemote ? ':remote' : (allowLocalOnly ? ':allow_local_only' : '')}${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { remote: !!onlyRemote, allow_local_only: !!allowLocalOnly, max_id: maxId, only_media: !!onlyMedia }, done); export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }, done); export const expandDirectTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('direct', '/api/v1/timelines/direct', { max_id: maxId }, done); -export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); +export const expandAccountTimeline = (accountId, { maxId, filter } = {}) => { + const path = filter ? filter : ''; + const params = { + include_replies: filter === ':replies', + include_reblogs: filter === ':reblogs', + only_reblogs: filter === ':reblogs', + mentions: filter === ':mentions', + max_id: maxId, + }; + + return expandTimeline(`account:${accountId}${path}`, `/api/v1/accounts/${accountId}/statuses`, params); +}; export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true }); export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 }); export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); diff --git a/app/javascript/flavours/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js index 890a422d3..7521d4645 100644 --- a/app/javascript/flavours/glitch/components/media_gallery.js +++ b/app/javascript/flavours/glitch/components/media_gallery.js @@ -385,6 +385,66 @@ class MediaGallery extends React.PureComponent { ); } + let parts = {}; + + media.map( + (attachment, i) => { + if (attachment.get('description')) { + if (attachment.get('description') in parts) { + parts[attachment.get('description')].push([i, attachment.get('url'), attachment.get('id')]); + } else { + parts[attachment.get('description')] = [[i, attachment.get('url'), attachment.get('id')]]; + } + } + }, + ); + + let descriptions = Object.entries(parts).map( + part => { + const [desc, idx] = part; + if (idx.length === 1) { + const url = idx[0][1]; + return ( + <p key={idx[0][2]}> + <strong> + <a href={url} title={url} target='_blank' rel='nofollow noopener'> + <FormattedMessage id='status.media.description' defaultMessage='Attachment #{index}: ' values={{ index: 1+idx[0][0] }} /> + </a> + </strong> + <span>{desc}</span> + </p> + ); + } else if (idx.length !== 0) { + const indexes = ( + <React.Fragment> + { + idx.map((i, c) => { + const url = i[1]; + return (<span key={i[2]}>{c === 0 ? ' ' : ', '}<a href={url} title={url} target='_blank' rel='nofollow noopener'>#{1+i[0]}</a></span>); + }) + } + </React.Fragment> + ); + return ( + <p key={idx[0][2]}> + <strong> + <FormattedMessage id='status.media.descriptions' defaultMessage='Attachments {list}: ' values={{ list: indexes }} /> + </strong> + <span>{desc}</span> + </p> + ); + } else { + return null; + } + }, + ); + + let description_wrapper = visible && ( + <div className='media-caption'> + {descriptions} + </div> + ); + return ( <div className={computedClass} style={style} ref={this.handleRef}> <div className={classNames('spoiler-button', { 'spoiler-button--minified': visible && !uncached, 'spoiler-button--click-thru': uncached })}> @@ -397,6 +457,7 @@ class MediaGallery extends React.PureComponent { </div> {children} + {description_wrapper} </div> ); } diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 1b7dce4c4..0de36ce2e 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -74,6 +74,8 @@ class Status extends ImmutablePureComponent { onReblog: PropTypes.func, onBookmark: PropTypes.func, onDelete: PropTypes.func, + onEdit: PropTypes.func, + onPublish: PropTypes.func, onDirect: PropTypes.func, onMention: PropTypes.func, onPin: PropTypes.func, @@ -373,7 +375,7 @@ class Status extends ImmutablePureComponent { } handleExpandedToggle = () => { - if (this.props.status.get('spoiler_text')) { + if (this.props.status.get('spoiler_text') || this.props.status.get('reblogSpoilerHtml')) { this.setExpansion(!this.state.isExpanded); } }; @@ -689,6 +691,9 @@ class Status extends ImmutablePureComponent { // Users can use those for theming, hiding avatars etc via UserStyle const selectorAttribs = { 'data-status-by': `@${status.getIn(['account', 'acct'])}`, + 'data-nest-level': status.get('nest_level'), + 'data-nest-deep': status.get('nest_level') >= 15, + 'data-local-only': !!status.get('local_only'), }; if (prepend && account) { @@ -710,6 +715,7 @@ class Status extends ImmutablePureComponent { const computedClass = classNames('status', `status-${status.get('visibility')}`, { collapsed: isCollapsed, + unpublished: status.get('published') === false, 'has-background': isCollapsed && background, 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index 2ccb02c62..b844590c0 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -13,6 +13,8 @@ import classNames from 'classnames'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' }, + edit: { id: 'status.edit', defaultMessage: 'Edit' }, + publish: { id: 'status.publish', defaultMessage: 'Publish' }, direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' }, mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, @@ -53,6 +55,8 @@ class StatusActionBar extends ImmutablePureComponent { onFavourite: PropTypes.func, onReblog: PropTypes.func, onDelete: PropTypes.func, + onEdit: PropTypes.func, + onPublish: PropTypes.func, onDirect: PropTypes.func, onMention: PropTypes.func, onMute: PropTypes.func, @@ -115,7 +119,7 @@ class StatusActionBar extends ImmutablePureComponent { _openInteractionDialog = type => { window.open(`/interact/${this.props.status.get('id')}?type=${type}`, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes'); - } + } handleDeleteClick = () => { this.props.onDelete(this.props.status, this.context.router.history); @@ -125,6 +129,14 @@ class StatusActionBar extends ImmutablePureComponent { this.props.onDelete(this.props.status, this.context.router.history, true); } + handleEditClick = () => { + this.props.onEdit(this.props.status, this.context.router.history); + } + + handlePublishClick = () => { + this.props.onPublish(this.props.status); + } + handlePinClick = () => { this.props.onPin(this.props.status); } @@ -211,10 +223,8 @@ class StatusActionBar extends ImmutablePureComponent { menu.push(null); - if (status.getIn(['account', 'id']) === me || withDismiss) { - menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); - menu.push(null); - } + menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); + menu.push(null); if (status.getIn(['account', 'id']) === me) { if (publicStatus) { @@ -223,6 +233,11 @@ class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); + menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick }); + + if (status.get('published') === false) { + menu.push({ text: intl.formatMessage(messages.publish), action: this.handlePublishClick }); + } } else { menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick }); diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js index a39f747b8..a4546edfd 100644 --- a/app/javascript/flavours/glitch/components/status_content.js +++ b/app/javascript/flavours/glitch/components/status_content.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import { isRtl } from 'flavours/glitch/util/rtl'; import { FormattedMessage } from 'react-intl'; import Permalink from './permalink'; +import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; import classnames from 'classnames'; import Icon from 'flavours/glitch/components/icon'; import { autoPlayGif } from 'flavours/glitch/util/initial_state'; @@ -13,7 +14,7 @@ const textMatchesTarget = (text, origin, host) => { return (text === origin || text === host || text.startsWith(origin + '/') || text.startsWith(host + '/') || 'www.' + text === host || ('www.' + text).startsWith(host + '/')); -} +}; const isLinkMisleading = (link) => { let linkTextParts = []; @@ -77,11 +78,13 @@ export default class StatusContent extends React.PureComponent { onUpdate: PropTypes.func, tagLinks: PropTypes.bool, rewriteMentions: PropTypes.string, + article: PropTypes.bool, }; static defaultProps = { tagLinks: true, rewriteMentions: 'no', + article: false, }; state = { @@ -231,7 +234,7 @@ export default class StatusContent extends React.PureComponent { let element = e.target; while (element) { - if (['button', 'video', 'a', 'label', 'canvas'].includes(element.localName)) { + if (['button', 'video', 'a', 'label', 'canvas', 'details', 'summary'].includes(element.localName)) { return; } element = element.parentNode; @@ -271,23 +274,213 @@ export default class StatusContent extends React.PureComponent { disabled, tagLinks, rewriteMentions, + article, } = this.props; const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden; - const content = { __html: status.get('contentHtml') }; - const spoilerContent = { __html: status.get('spoilerHtml') }; + const edited = (status.get('edited') === 0) ? null : ( + <div className='status__notice status__edit-notice'> + <Icon id='pencil-square-o' /> + <FormattedMessage + id='status.edited' + defaultMessage='{count, plural, one {# edit} other {# edits}} · last update: {updated_at}' + key={`edit-${status.get('id')}`} + values={{ + count: status.get('edited'), + updated_at: <RelativeTimestamp timestamp={status.get('updated_at')} />, + }} + /> + </div> + ); + + const unpublished = (status.get('published') === false) && ( + <div className='status__notice status__unpublished-notice'> + <Icon id='chain-broken' /> + <FormattedMessage + id='status.unpublished' + defaultMessage='Unpublished' + key={`unpublished-${status.get('id')}`} + /> + </div> + ); + + const local_only = (status.get('local_only') === true) && ( + <div className='status__notice status__localonly-notice'> + <Icon id='home' /> + <FormattedMessage + id='advanced_options.local-only.short' + defaultMessage='Local-only' + key={`localonly-${status.get('id')}`} + /> + </div> + ); + + const quiet = (status.get('notify') === false) && ( + <div className='status__notice status__quiet-notice'> + <Icon id='bell-slash' /> + <FormattedMessage + id='status.quiet' + defaultMessage='Quiet local publish' + key={`quiet-${status.get('id')}`} + /> + </div> + ); + + const article_content = status.get('article') && ( + <div className='status__notice status__article-notice'> + <Icon id='file-text-o' /> + <Permalink + href={status.get('url')} + to={`/statuses/${status.get('id')}`} + > + <FormattedMessage + id='status.article' + defaultMessage='Article' + key={`article-${status.get('id')}`} + /> + </Permalink> + </div> + ); + + const publish_at = status.get('publish_at') && ( + <div className='status__notice status__publish-notice'> + <Icon id='bullhorn' /> + <FormattedMessage + id='status.publish_at' + defaultMessage='Auto-publish: {publish_at}' + key={`publish-${status.get('id')}`} + values={{ + publish_at: <RelativeTimestamp timestamp={status.get('publish_at')} futureDate />, + }} + /> + </div> + ); + + const expires_at = !unpublished && status.get('expires_at') && ( + <div className='status__notice status__expires-notice'> + <Icon id='clock-o' /> + <FormattedMessage + id='status.expires_at' + defaultMessage='Self-destruct: {expires_at}' + key={`expires-${status.get('id')}`} + values={{ + expires_at: <RelativeTimestamp timestamp={status.get('expires_at')} futureDate />, + }} + /> + </div> + ); + + const status_notice_wrapper = ( + <div className='status__notice-wrapper'> + {unpublished} + {publish_at} + {expires_at} + {quiet} + {edited} + {local_only} + {article_content} + </div> + ); + + const permissions_present = status.get('domain_permissions') && status.get('domain_permissions').size > 0; + + const status_permission_items = permissions_present && status.get('domain_permissions').map((permission) => ( + <li className='permission-status'> + <Icon id='eye-slash' /> + <FormattedMessage + id='status.permissions.visibility.status' + defaultMessage='{visibility} 🡲 {domain}' + key={`permissions-visibility-${status.get('id')}`} + values={{ + domain: <span>{permission.get('domain')}</span>, + visibility: <span>{permission.get('visibility')}</span>, + }} + /> + </li> + )); + + const permissions = status_permission_items && ( + <details className='status__permissions' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}> + <summary> + <Icon id='unlock-alt' /> + <FormattedMessage + id='status.permissions.title' + defaultMessage='Show extended permissions...' + key={`permissions-${status.get('id')}`} + /> + </summary> + <ul> + {status_permission_items} + </ul> + </details> + ); + + const tag_items = (status.get('tags') && status.get('tags').size > 0) && status.get('tags').map(hashtag => + ( + <li> + <Icon id='tag' /> + <Permalink + href={hashtag.get('url')} + to={`/timelines/tag/${hashtag.get('name')}`} + > + <span>{hashtag.get('name')}</span> + </Permalink> + </li> + )); + + const tags = tag_items && ( + <details className='status__tags' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}> + <summary> + <Icon id='tag' /> + <FormattedMessage + id='status.tags' + defaultMessage='Show all tags...' + key={`tags-${status.get('id')}`} + /> + </summary> + <ul> + {tag_items} + </ul> + </details> + ); + + const footers = ( + <div className='status__footers'> + {permissions} + {tags} + </div> + ); + + const reblog_spoiler_html = status.get('reblogSpoilerPresent') && { __html: status.get('reblogSpoilerHtml') }; + const reblog_spoiler = reblog_spoiler_html && ( + <div className='reblog-spoiler spoiler'> + <Icon id='retweet' /> + <span dangerouslySetInnerHTML={reblog_spoiler_html} /> + </div> + ); + + const spoiler_html = status.get('spoiler_text').length > 0 && { __html: status.get('spoilerHtml') }; + const spoiler = spoiler_html && ( + <div className='spoiler'> + <Icon id='info-circle' /> + <span dangerouslySetInnerHTML={spoiler_html} /> + </div> + ); + + const spoiler_present = status.get('spoiler_text').length > 0 || status.get('reblogSpoilerPresent'); + const content = { __html: article ? status.get('articleHtml') : status.get('contentHtml') }; const directionStyle = { direction: 'ltr' }; const classNames = classnames('status__content', { 'status__content--with-action': parseClick && !disabled, - 'status__content--with-spoiler': status.get('spoiler_text').length > 0, + 'status__content--with-spoiler': spoiler_present, }); if (isRtl(status.get('search_index'))) { directionStyle.direction = 'rtl'; } - if (status.get('spoiler_text').length > 0) { + if (spoiler_present) { let mentionsPlaceholder = ''; const mentionLinks = status.get('mentions').map(item => ( @@ -302,11 +495,19 @@ export default class StatusContent extends React.PureComponent { )).reduce((aggregate, item) => [...aggregate, item, ' '], []); const toggleText = hidden ? [ - <FormattedMessage - id='status.show_more' - defaultMessage='Show more' - key='0' - />, + article ? ( + <FormattedMessage + id='status.show_article' + defaultMessage='Show article' + key='0' + /> + ) : ( + <FormattedMessage + id='status.show_more' + defaultMessage='Show more' + key='0' + /> + ), mediaIcon ? ( <Icon fixedWidth @@ -330,15 +531,18 @@ export default class StatusContent extends React.PureComponent { return ( <div className={classNames} tabIndex='0' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} ref={this.setRef}> - <p + {status_notice_wrapper} + <div style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }} > - <span dangerouslySetInnerHTML={spoilerContent} /> - {' '} - <button tabIndex='0' className='status__content__spoiler-link' onClick={this.handleSpoilerClick}> - {toggleText} - </button> - </p> + {reblog_spoiler} + {spoiler} + <div class='spoiler-actions'> + <button tabIndex='0' className='status__content__spoiler-link' onClick={this.handleSpoilerClick}> + {toggleText} + </button> + </div> + </div> {mentionsPlaceholder} @@ -354,6 +558,8 @@ export default class StatusContent extends React.PureComponent { {media} </div> + {footers} + </div> ); } else if (parseClick) { @@ -366,6 +572,7 @@ export default class StatusContent extends React.PureComponent { tabIndex='0' ref={this.setRef} > + {status_notice_wrapper} <div ref={this.setContentsRef} key={`contents-${tagLinks}-${rewriteMentions}`} @@ -374,6 +581,7 @@ export default class StatusContent extends React.PureComponent { tabIndex='0' /> {media} + {footers} </div> ); } else { @@ -384,8 +592,10 @@ export default class StatusContent extends React.PureComponent { tabIndex='0' ref={this.setRef} > + {status_notice_wrapper} <div ref={this.setContentsRef} key={`contents-${tagLinks}`} className='status__content__text' dangerouslySetInnerHTML={content} tabIndex='0' /> {media} + {footers} </div> ); } diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index 7782246a6..5d60ffd97 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -17,7 +17,7 @@ import { pin, unpin, } from 'flavours/glitch/actions/interactions'; -import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses'; +import { muteStatus, unmuteStatus, deleteStatus, editStatus, publishStatus } from 'flavours/glitch/actions/statuses'; import { initMuteModal } from 'flavours/glitch/actions/mutes'; import { initBlockModal } from 'flavours/glitch/actions/blocks'; import { initReport } from 'flavours/glitch/actions/reports'; @@ -39,6 +39,8 @@ const messages = defineMessages({ redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, + publishConfirm: { id: 'confirmations.publish.confirm', defaultMessage: 'Publish' }, + publishMessage: { id: 'confirmations.publish.message', defaultMessage: 'Are you ready to publish your post?' }, unfilterConfirm: { id: 'confirmations.unfilter.confirm', defaultMessage: 'Show' }, author: { id: 'confirmations.unfilter.author', defaultMessage: 'Author' }, matchingFilters: { id: 'confirmations.unfilter.filters', defaultMessage: 'Matching {count, plural, one {filter} other {filters}}' }, @@ -168,6 +170,18 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ } }, + onEdit (status, history) { + dispatch(editStatus(status, history)); + }, + + onPublish (status) { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.publishMessage), + confirm: intl.formatMessage(messages.publishConfirm), + onConfirm: () => dispatch(publishStatus(status.get('id'))), + })); + }, + onDirect (account, router) { dispatch(directCompose(account, router)); }, diff --git a/app/javascript/flavours/glitch/features/account/components/action_bar.js b/app/javascript/flavours/glitch/features/account/components/action_bar.js index 2d4cc7f49..0f1b83b2d 100644 --- a/app/javascript/flavours/glitch/features/account/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/account/components/action_bar.js @@ -44,14 +44,20 @@ class ActionBar extends React.PureComponent { if (account.get('acct') !== account.get('username')) { extraInfo = ( <div className='account__disclaimer'> - <Icon id='info-circle' fixedWidth /> <FormattedMessage - id='account.disclaimer_full' - defaultMessage="Information below may reflect the user's profile incompletely." - /> - {' '} - <a target='_blank' rel='noopener' href={account.get('url')}> - <FormattedMessage id='account.view_full_profile' defaultMessage='View full profile' /> - </a> + <p> + <Icon id='info-circle' fixedWidth /> <FormattedMessage + id='account.disclaimer_full' + defaultMessage="Information below may reflect the user's profile incompletely." + /> + </p> + <p> + <Icon id='link' fixedWidth /> <a target='_blank' rel='noopener' href={account.get('url')}> + <FormattedMessage + id='account.view_full_profile' + defaultMessage='View full profile' + /> + </a> + </p> </div> ); } @@ -64,17 +70,14 @@ class ActionBar extends React.PureComponent { <div className='account__action-bar-links'> <NavLink isActive={this.isStatusesPageActive} activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}`}> <FormattedMessage id='account.posts' defaultMessage='Posts' /> - <strong><FormattedNumber value={account.get('statuses_count')} /></strong> </NavLink> <NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`}> <FormattedMessage id='account.follows' defaultMessage='Follows' /> - <strong><FormattedNumber value={account.get('following_count')} /></strong> </NavLink> <NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`}> <FormattedMessage id='account.followers' defaultMessage='Followers' /> - <strong>{ account.get('followers_count') < 0 ? '-' : <FormattedNumber value={account.get('followers_count')} /> }</strong> </NavLink> </div> </div> diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/header.js b/app/javascript/flavours/glitch/features/account_timeline/components/header.js index a6b57d331..d399d4aa9 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/header.js +++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.js @@ -7,6 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { FormattedMessage } from 'react-intl'; import { NavLink } from 'react-router-dom'; import MovedNote from './moved_note'; +import { me } from 'flavours/glitch/util/initial_state'; export default class Header extends ImmutablePureComponent { @@ -128,9 +129,12 @@ export default class Header extends ImmutablePureComponent { {!hideTabs && ( <div className='account__section-headline'> - <NavLink exact to={`/accounts/${account.get('id')}`}><FormattedMessage id='account.posts' defaultMessage='Toots' /></NavLink> - <NavLink exact to={`/accounts/${account.get('id')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots with replies' /></NavLink> + <NavLink exact to={`/accounts/${account.get('id')}`}><FormattedMessage id='account.threads' defaultMessage='Threads' /></NavLink> + { (account.get('id') === me || account.get('show_replies')) && + (<NavLink exact to={`/accounts/${account.get('id')}/with_replies`}><FormattedMessage id='account.posts_with_replies' defaultMessage='Toots with replies' /></NavLink>) } + { (account.get('id') !== me) && (<NavLink exact to={`/accounts/${account.get('id')}/mentions`}><FormattedMessage id='account.mentions' defaultMessage='Mentions' /></NavLink>) } <NavLink exact to={`/accounts/${account.get('id')}/media`}><FormattedMessage id='account.media' defaultMessage='Media' /></NavLink> + <NavLink exact to={`/accounts/${account.get('id')}/reblogs`}><FormattedMessage id='account.reblogs' defaultMessage='Boosts' /></NavLink> </div> )} </div> diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.js index c56cc9b8e..c88f6ac89 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/index.js +++ b/app/javascript/flavours/glitch/features/account_timeline/index.js @@ -19,15 +19,15 @@ import TimelineHint from 'flavours/glitch/components/timeline_hint'; const emptyList = ImmutableList(); -const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => { - const path = withReplies ? `${accountId}:with_replies` : accountId; +const mapStateToProps = (state, { params: { accountId }, filter = '' }) => { + const path = `${accountId}${filter}`; return { remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])), remoteUrl: state.getIn(['accounts', accountId, 'url']), isAccount: !!state.getIn(['accounts', accountId]), statusIds: state.getIn(['timelines', `account:${path}`, 'items'], ImmutableList()), - featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()), + featuredStatusIds: !filter ? state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], ImmutableList()) : ImmutableList(), isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']), hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']), suspended: state.getIn(['accounts', accountId, 'suspended'], false), @@ -52,7 +52,7 @@ class AccountTimeline extends ImmutablePureComponent { featuredStatusIds: ImmutablePropTypes.list, isLoading: PropTypes.bool, hasMore: PropTypes.bool, - withReplies: PropTypes.bool, + filter: PropTypes.string, isAccount: PropTypes.bool, suspended: PropTypes.bool, remote: PropTypes.bool, @@ -61,24 +61,24 @@ class AccountTimeline extends ImmutablePureComponent { }; componentWillMount () { - const { params: { accountId }, withReplies } = this.props; + const { params: { accountId }, filter } = this.props; this.props.dispatch(fetchAccount(accountId)); this.props.dispatch(fetchAccountIdentityProofs(accountId)); - if (!withReplies) { + if (!filter) { this.props.dispatch(expandAccountFeaturedTimeline(accountId)); } - this.props.dispatch(expandAccountTimeline(accountId, { withReplies })); + this.props.dispatch(expandAccountTimeline(accountId, { filter })); } componentWillReceiveProps (nextProps) { - if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.withReplies !== this.props.withReplies) { + if ((nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) || nextProps.filter !== this.props.filter) { this.props.dispatch(fetchAccount(nextProps.params.accountId)); this.props.dispatch(fetchAccountIdentityProofs(nextProps.params.accountId)); - if (!nextProps.withReplies) { + if (!nextProps.filter) { this.props.dispatch(expandAccountFeaturedTimeline(nextProps.params.accountId)); } - this.props.dispatch(expandAccountTimeline(nextProps.params.accountId, { withReplies: nextProps.params.withReplies })); + this.props.dispatch(expandAccountTimeline(nextProps.params.accountId, { filter: nextProps.params.filter })); } } @@ -87,7 +87,7 @@ class AccountTimeline extends ImmutablePureComponent { } handleLoadMore = maxId => { - this.props.dispatch(expandAccountTimeline(this.props.params.accountId, { maxId, withReplies: this.props.withReplies })); + this.props.dispatch(expandAccountTimeline(this.props.params.accountId, { maxId, filter: this.props.filter })); } setRef = c => { diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.js b/app/javascript/flavours/glitch/features/compose/components/compose_form.js index a7cb95222..e812ba982 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.js +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.js @@ -63,6 +63,8 @@ class ComposeForm extends ImmutablePureComponent { layout: PropTypes.string, media: ImmutablePropTypes.list, sideArm: PropTypes.string, + sideArmWarning: PropTypes.bool, + privacyWarning: PropTypes.bool, sensitive: PropTypes.bool, spoilersAlwaysOn: PropTypes.bool, mediaDescriptionConfirmation: PropTypes.bool, @@ -71,10 +73,12 @@ class ComposeForm extends ImmutablePureComponent { onChangeVisibility: PropTypes.func, onPaste: PropTypes.func, onMediaDescriptionConfirm: PropTypes.func, + clearTimeout: PropTypes.bool, }; static defaultProps = { showSearch: false, + clearTimeout: null, }; handleChange = (e) => { @@ -149,6 +153,17 @@ class ComposeForm extends ImmutablePureComponent { this.handleSubmit(sideArm === 'none' ? null : sideArm); } + handleClearAll = () => { + if(!this.clearTimeout || this.clearTimeout === null) { + this.clearTimeout = window.setTimeout(() => { + this.clearTimeout = null; + }, 500); + } else { + this.clearTimeout = null; + this.props.onClearAll(); + } + } + // Selects a suggestion from the autofill. onSuggestionSelected = (tokenStart, token, value) => { this.props.onSuggestionSelected(tokenStart, token, value, ['text']); @@ -256,6 +271,7 @@ class ComposeForm extends ImmutablePureComponent { handleSecondarySubmit, handleSelect, handleSubmit, + handleClearAll, handleRefTextarea, } = this; const { @@ -273,19 +289,22 @@ class ComposeForm extends ImmutablePureComponent { onFetchSuggestions, onPaste, privacy, + privacyWarning, sensitive, showSearch, sideArm, + sideArmWarning, spoiler, spoilerText, suggestions, text, spoilersAlwaysOn, + clearTimeout, } = this.props; let disabledButton = isSubmitting || isUploading || isChangingUpload || (!text.trim().length && !anyMedia); - const countText = `${spoilerText}${countableText(text)}${advancedOptions && advancedOptions.get('do_not_federate') ? ' 👁️' : ''}`; + const countText = `${spoilerText}${countableText(text)}`; return ( <div className='composer'> @@ -356,8 +375,11 @@ class ComposeForm extends ImmutablePureComponent { disabled={disabledButton} onSecondarySubmit={handleSecondarySubmit} onSubmit={handleSubmit} + onClearAll={handleClearAll} privacy={privacy} + privacyWarning={privacyWarning} sideArm={sideArm} + sideArmWarning={sideArmWarning} /> </div> ); diff --git a/app/javascript/flavours/glitch/features/compose/components/publisher.js b/app/javascript/flavours/glitch/features/compose/components/publisher.js index 97890f40d..d42c578aa 100644 --- a/app/javascript/flavours/glitch/features/compose/components/publisher.js +++ b/app/javascript/flavours/glitch/features/compose/components/publisher.js @@ -23,6 +23,10 @@ const messages = defineMessages({ defaultMessage: '{publish}!', id: 'compose_form.publish_loud', }, + clear: { + defaultMessage: 'Double-click to clear', + id: 'compose_form.clear', + }, }); export default @injectIntl @@ -34,8 +38,11 @@ class Publisher extends ImmutablePureComponent { intl: PropTypes.object.isRequired, onSecondarySubmit: PropTypes.func, onSubmit: PropTypes.func, + onClearAll: PropTypes.func, privacy: PropTypes.oneOf(['direct', 'private', 'unlisted', 'public']), + privacyWarning: PropTypes.bool, sideArm: PropTypes.oneOf(['none', 'direct', 'private', 'unlisted', 'public']), + sideArmWarning: PropTypes.bool, }; handleSubmit = () => { @@ -43,7 +50,7 @@ class Publisher extends ImmutablePureComponent { }; render () { - const { countText, disabled, intl, onSecondarySubmit, privacy, sideArm } = this.props; + const { countText, disabled, intl, onClearAll, onSecondarySubmit, privacy, privacyWarning, sideArm, sideArmWarning } = this.props; const diff = maxChars - length(countText || ''); const computedClass = classNames('composer--publisher', { @@ -53,9 +60,20 @@ class Publisher extends ImmutablePureComponent { return ( <div className={computedClass}> + <Button + className='clear' + onClick={onClearAll} + style={{ padding: null }} + title={intl.formatMessage(messages.clear)} + text={ + <span> + <Icon id='trash-o' /> + </span> + } + /> {sideArm && sideArm !== 'none' ? ( <Button - className='side_arm' + className={classNames('side_arm', {privacy_warning: sideArmWarning})} disabled={disabled || diff < 0} onClick={onSecondarySubmit} style={{ padding: null }} @@ -75,7 +93,7 @@ class Publisher extends ImmutablePureComponent { /> ) : null} <Button - className='primary' + className={classNames('primary', {privacy_warning: privacyWarning})} text={function () { switch (true) { case !!sideArm && sideArm !== 'none': diff --git a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js index fcd2caf1b..cf953ec3d 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js @@ -12,13 +12,14 @@ import { selectComposeSuggestion, submitCompose, uploadCompose, + resetCompose, } from 'flavours/glitch/actions/compose'; import { openModal, } from 'flavours/glitch/actions/modal'; import { changeLocalSetting } from 'flavours/glitch/actions/local_settings'; -import { privacyPreference } from 'flavours/glitch/util/privacy_preference'; +import { privacyPreference, order as privacyOrder } from 'flavours/glitch/util/privacy_preference'; const messages = defineMessages({ missingDescriptionMessage: { id: 'confirmations.missing_media_description.message', @@ -57,7 +58,9 @@ function mapStateToProps (state) { media: state.getIn(['compose', 'media_attachments']), preselectDate: state.getIn(['compose', 'preselectDate']), privacy: state.getIn(['compose', 'privacy']), + privacyWarning: replyPrivacy && privacyOrder.indexOf(state.getIn(['compose', 'privacy'])) < privacyOrder.indexOf(replyPrivacy), sideArm: sideArmPrivacy, + sideArmWarning: sideArmPrivacy && sideArmRestrictedPrivacy && privacyOrder.indexOf(sideArmPrivacy) < privacyOrder.indexOf(sideArmRestrictedPrivacy), sensitive: state.getIn(['compose', 'sensitive']), showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']), spoiler: spoilersAlwaysOn || state.getIn(['compose', 'spoiler']), @@ -82,6 +85,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(submitCompose(routerHistory)); }, + onClearAll() { + dispatch(resetCompose()); + }, + onClearSuggestions() { dispatch(clearComposeSuggestions()); }, diff --git a/app/javascript/flavours/glitch/features/getting_started/index.js b/app/javascript/flavours/glitch/features/getting_started/index.js index b4549fdf8..43d535ac5 100644 --- a/app/javascript/flavours/glitch/features/getting_started/index.js +++ b/app/javascript/flavours/glitch/features/getting_started/index.js @@ -46,7 +46,10 @@ const makeMapStateToProps = () => { return lists; } - return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title'))); + return lists.toList().filter(item => !!item).sort((a, b) => { + const r = (b.get('reblogs') ? 1 : 0) - (a.get('reblogs') ? 1 : 0); + return r === 0 ? a.get('title').localeCompare(b.get('title')) : r; + }); }); const mapStateToProps = state => ({ diff --git a/app/javascript/flavours/glitch/features/list_adder/index.js b/app/javascript/flavours/glitch/features/list_adder/index.js index cb8a15e8c..b7f3d1ef7 100644 --- a/app/javascript/flavours/glitch/features/list_adder/index.js +++ b/app/javascript/flavours/glitch/features/list_adder/index.js @@ -16,7 +16,10 @@ const getOrderedLists = createSelector([state => state.get('lists')], lists => { return lists; } - return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title'))); + return lists.toList().filter(item => !!item).sort((a, b) => { + const r = (b.get('reblogs') ? 1 : 0) - (a.get('reblogs') ? 1 : 0); + return r === 0 ? a.get('title').localeCompare(b.get('title')) : r; + }); }); const mapStateToProps = state => ({ diff --git a/app/javascript/flavours/glitch/features/lists/index.js b/app/javascript/flavours/glitch/features/lists/index.js index e384f301b..3863b8e25 100644 --- a/app/javascript/flavours/glitch/features/lists/index.js +++ b/app/javascript/flavours/glitch/features/lists/index.js @@ -24,7 +24,10 @@ const getOrderedLists = createSelector([state => state.get('lists')], lists => { return lists; } - return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title'))); + return lists.toList().filter(item => !!item).sort((a, b) => { + const r = (b.get('reblogs') ? 1 : 0) - (a.get('reblogs') ? 1 : 0); + return r === 0 ? a.get('title').localeCompare(b.get('title')) : r; + }); }); const mapStateToProps = state => ({ diff --git a/app/javascript/flavours/glitch/features/notifications/components/follow.js b/app/javascript/flavours/glitch/features/notifications/components/follow.js index 0d3162fc9..7b47f411b 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/follow.js +++ b/app/javascript/flavours/glitch/features/notifications/components/follow.js @@ -84,11 +84,13 @@ export default class NotificationFollow extends ImmutablePureComponent { <Icon fixedWidth id='user-plus' /> </div> - <FormattedMessage - id='notification.follow' - defaultMessage='{name} followed you' - values={{ name: link }} - /> + <span title={notification.get('created_at')}> + <FormattedMessage + id='notification.follow' + defaultMessage='{name} followed you' + values={{ name: link }} + /> + </span> </div> <AccountContainer hidden={hidden} id={account.get('id')} withNote={false} /> diff --git a/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.js b/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.js index 73fc05dea..4cac53266 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.js +++ b/app/javascript/flavours/glitch/features/notifications/components/notifications_permission_banner.js @@ -20,6 +20,10 @@ class NotificationsPermissionBanner extends React.PureComponent { intl: PropTypes.object.isRequired, }; + handleClickDisable = () => { + window.location = '/settings/preferences/notifications'; + } + handleClick = () => { this.props.dispatch(requestBrowserPermission()); } @@ -39,7 +43,8 @@ class NotificationsPermissionBanner extends React.PureComponent { <h2><FormattedMessage id='notifications_permission_banner.title' defaultMessage='Never miss a thing' /></h2> <p><FormattedMessage id='notifications_permission_banner.how_to_control' defaultMessage="To receive notifications when Mastodon isn't open, enable desktop notifications. You can control precisely which types of interactions generate desktop notifications through the {icon} button above once they're enabled." values={{ icon: <Icon id='sliders' /> }} /></p> - <Button onClick={this.handleClick}><FormattedMessage id='notifications_permission_banner.enable' defaultMessage='Enable desktop notifications' /></Button> + <Button onClick={this.handleClick}><FormattedMessage id='notifications_permission_banner.enable' defaultMessage='Request' /></Button> + <Button onClick={this.handleClickDisable}><FormattedMessage id='notifications_permission_banner.disable' defaultMessage='Disable' /></Button> </div> ); } diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js index 0f16d93fe..b2c8ac87f 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js @@ -11,6 +11,8 @@ import classNames from 'classnames'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' }, + edit: { id: 'status.edit', defaultMessage: 'Edit' }, + publish: { id: 'status.publish', defaultMessage: 'Publish' }, direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' }, mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, reply: { id: 'status.reply', defaultMessage: 'Reply' }, @@ -52,6 +54,8 @@ class ActionBar extends React.PureComponent { onMuteConversation: PropTypes.func, onBlock: PropTypes.func, onDelete: PropTypes.func.isRequired, + onEdit: PropTypes.func.isRequired, + onPublish: PropTypes.func.isRequired, onDirect: PropTypes.func.isRequired, onMention: PropTypes.func.isRequired, onReport: PropTypes.func, @@ -84,6 +88,14 @@ class ActionBar extends React.PureComponent { this.props.onDelete(this.props.status, this.context.router.history, true); } + handleEditClick = () => { + this.props.onEdit(this.props.status, this.context.router.history); + } + + handlePublishClick = () => { + this.props.onPublish(this.props.status); + } + handleDirectClick = () => { this.props.onDirect(this.props.status.get('account'), this.context.router.history); } @@ -166,6 +178,11 @@ class ActionBar extends React.PureComponent { menu.push(null); menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); + menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick }); + + if (status.get('published') === false) { + menu.push({ text: intl.formatMessage(messages.publish), action: this.handlePublishClick }); + } } else { menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick }); diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js index 04d350bcb..d03a425eb 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -17,7 +17,7 @@ import scheduleIdleTask from 'flavours/glitch/util/schedule_idle_task'; import classNames from 'classnames'; import PollContainer from 'flavours/glitch/containers/poll_container'; import Icon from 'flavours/glitch/components/icon'; -import AnimatedNumber from 'flavours/glitch/components/animated_number'; +import { me } from 'flavours/glitch/util/initial_state'; import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder'; export default class DetailedStatus extends ImmutablePureComponent { @@ -200,7 +200,7 @@ export default class DetailedStatus extends ImmutablePureComponent { applicationLink = <React.Fragment> · <a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></React.Fragment>; } - const visibilityLink = <React.Fragment> · <VisibilityIcon visibility={status.get('visibility')} /></React.Fragment>; + const visibilityLink = <React.Fragment><VisibilityIcon visibility={status.get('visibility')} /> · </React.Fragment>; if (status.get('visibility') === 'direct') { reblogIcon = 'envelope'; @@ -208,7 +208,7 @@ export default class DetailedStatus extends ImmutablePureComponent { reblogIcon = 'lock'; } - if (!['unlisted', 'public'].includes(status.get('visibility'))) { + if (status.getIn(['account', 'id']) !== me || !['unlisted', 'public'].includes(status.get('visibility'))) { reblogLink = null; } else if (this.context.router) { reblogLink = ( @@ -216,9 +216,6 @@ export default class DetailedStatus extends ImmutablePureComponent { <React.Fragment> · </React.Fragment> <Link to={`/statuses/${status.get('id')}/reblogs`} className='detailed-status__link'> <Icon id={reblogIcon} /> - <span className='detailed-status__reblogs'> - <AnimatedNumber value={status.get('reblogs_count')} /> - </span> </Link> </React.Fragment> ); @@ -228,37 +225,43 @@ export default class DetailedStatus extends ImmutablePureComponent { <React.Fragment> · </React.Fragment> <a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}> <Icon id={reblogIcon} /> - <span className='detailed-status__reblogs'> - <AnimatedNumber value={status.get('reblogs_count')} /> - </span> </a> </React.Fragment> ); } - if (this.context.router) { + if (status.getIn(['account', 'id']) !== me) { + favouriteLink = null; + } else if (this.context.router) { favouriteLink = ( - <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'> - <Icon id='star' /> - <span className='detailed-status__favorites'> - <AnimatedNumber value={status.get('favourites_count')} /> - </span> - </Link> + <React.Fragment> + <React.Fragment> · </React.Fragment> + <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'> + <Icon id='star' /> + </Link> + </React.Fragment> ); } else { favouriteLink = ( - <a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}> - <Icon id='star' /> - <span className='detailed-status__favorites'> - <AnimatedNumber value={status.get('favourites_count')} /> - </span> - </a> + <React.Fragment> + <React.Fragment> · </React.Fragment> + <a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}> + <Icon id='star' /> + </a> + </React.Fragment> ); } + const selectorAttribs = { + 'data-status-by': `@${status.getIn(['account', 'acct'])}`, + 'data-nest-level': status.get('nest_level'), + 'data-nest-deep': status.get('nest_level') >= 15, + 'data-local-only': !!status.get('local_only'), + }; + return ( <div style={outerStyle}> - <div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })} data-status-by={status.getIn(['account', 'acct'])}> + <div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact, unpublished: status.get('published') === false })} {...selectorAttribs}> <a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='detailed-status__display-name'> <div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={48} /></div> <DisplayName account={status.get('account')} localDomain={this.props.domain} /> @@ -275,13 +278,15 @@ export default class DetailedStatus extends ImmutablePureComponent { onUpdate={this.handleChildUpdate} tagLinks={settings.get('tag_misleading_links')} rewriteMentions={settings.get('rewrite_mentions')} + article disabled /> <div className='detailed-status__meta'> + {visibilityLink} <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'> <FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' /> - </a>{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink} + </a>{applicationLink}{reblogLink}{favouriteLink} </div> </div> </div> diff --git a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js index 9d11f37e0..124de903a 100644 --- a/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js +++ b/app/javascript/flavours/glitch/features/status/containers/detailed_status_container.js @@ -17,7 +17,9 @@ import { import { muteStatus, unmuteStatus, + editStatus, deleteStatus, + publishStatus, hideStatus, revealStatus, } from 'flavours/glitch/actions/statuses'; @@ -34,6 +36,8 @@ const messages = defineMessages({ deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' }, + publishConfirm: { id: 'confirmations.publish.confirm', defaultMessage: 'Publish' }, + publishMessage: { id: 'confirmations.publish.message', defaultMessage: 'Are you ready to publish your post?' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, }); @@ -118,6 +122,18 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, + onEdit (status, history) { + dispatch(editStatus(status, history)); + }, + + onPublish (status) { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.publishMessage), + confirm: intl.formatMessage(messages.publishConfirm), + onConfirm: () => dispatch(publishStatus(status.get('id'))), + })); + }, + onDirect (account, router) { dispatch(directCompose(account, router)); }, diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js index b330adf3f..74d51f8f8 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -26,7 +26,7 @@ import { directCompose, } from 'flavours/glitch/actions/compose'; import { changeLocalSetting } from 'flavours/glitch/actions/local_settings'; -import { muteStatus, unmuteStatus, deleteStatus } from 'flavours/glitch/actions/statuses'; +import { muteStatus, unmuteStatus, deleteStatus, editStatus, publishStatus } from 'flavours/glitch/actions/statuses'; import { initMuteModal } from 'flavours/glitch/actions/mutes'; import { initBlockModal } from 'flavours/glitch/actions/blocks'; import { initReport } from 'flavours/glitch/actions/reports'; @@ -50,6 +50,8 @@ const messages = defineMessages({ deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' }, redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' }, redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? You will lose all replies, boosts and favourites to it.' }, + publishConfirm: { id: 'confirmations.publish.confirm', defaultMessage: 'Publish' }, + publishMessage: { id: 'confirmations.publish.message', defaultMessage: 'Are you ready to publish your post?' }, revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' }, hideAll: { id: 'status.show_less_all', defaultMessage: 'Show less for all' }, detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' }, @@ -306,6 +308,20 @@ class Status extends ImmutablePureComponent { } } + handleEditClick = (status, history) => { + this.props.dispatch(editStatus(status, history)); + } + + handlePublishClick = (status) => { + const { dispatch, intl } = this.props; + + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.publishMessage), + confirm: intl.formatMessage(messages.publishConfirm), + onConfirm: () => dispatch(publishStatus(status.get('id'))), + })); + } + handleDirectClick = (account, router) => { this.props.dispatch(directCompose(account, router)); } @@ -591,6 +607,8 @@ class Status extends ImmutablePureComponent { onReblog={this.handleReblogClick} onBookmark={this.handleBookmarkClick} onDelete={this.handleDeleteClick} + onEdit={this.handleEditClick} + onPublish={this.handlePublishClick} onDirect={this.handleDirectClick} onMention={this.handleMentionClick} onMute={this.handleMuteClick} diff --git a/app/javascript/flavours/glitch/features/ui/components/link_footer.js b/app/javascript/flavours/glitch/features/ui/components/link_footer.js index 4d7fc36c2..f8a61d2fb 100644 --- a/app/javascript/flavours/glitch/features/ui/components/link_footer.js +++ b/app/javascript/flavours/glitch/features/ui/components/link_footer.js @@ -60,6 +60,7 @@ class LinkFooter extends React.PureComponent { id='getting_started.open_source_notice' defaultMessage='Glitchsoc is open source software, a friendly fork of {Mastodon}. You can contribute or report issues on GitHub at {github}.' values={{ + monsterware: <span><a href='https://monsterware.dev/monsterpit/monsterpit-mastodon' rel='noopener noreferrer' target='_blank'>MonsterWare</a></span>, github: <span><a href='https://github.com/glitch-soc/mastodon' rel='noopener noreferrer' target='_blank'>glitch-soc/mastodon</a> (v{version})</span>, Mastodon: <a href='https://github.com/tootsuite/mastodon' rel='noopener noreferrer' target='_blank'>Mastodon</a> }} /> diff --git a/app/javascript/flavours/glitch/features/ui/components/list_panel.js b/app/javascript/flavours/glitch/features/ui/components/list_panel.js index 354e35027..f351e2a01 100644 --- a/app/javascript/flavours/glitch/features/ui/components/list_panel.js +++ b/app/javascript/flavours/glitch/features/ui/components/list_panel.js @@ -13,7 +13,10 @@ const getOrderedLists = createSelector([state => state.get('lists')], lists => { return lists; } - return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title'))).take(4); + return lists.toList().filter(item => !!item).sort((a, b) => { + const r = (b.get('reblogs') ? 1 : 0) - (a.get('reblogs') ? 1 : 0); + return r === 0 ? a.get('title').localeCompare(b.get('title')) : r; + }); }); const mapStateToProps = state => ({ diff --git a/app/javascript/flavours/glitch/features/ui/components/mute_modal.js b/app/javascript/flavours/glitch/features/ui/components/mute_modal.js index 7d25db316..5970ceddb 100644 --- a/app/javascript/flavours/glitch/features/ui/components/mute_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/mute_modal.js @@ -7,6 +7,7 @@ import Button from 'flavours/glitch/components/button'; import { closeModal } from 'flavours/glitch/actions/modal'; import { muteAccount } from 'flavours/glitch/actions/accounts'; import { toggleHideNotifications, changeMuteDuration } from 'flavours/glitch/actions/mutes'; +import { toggleTimelinesOnly } from 'flavours/glitch/actions/mutes'; const messages = defineMessages({ minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' }, @@ -20,13 +21,14 @@ const mapStateToProps = state => { account: state.getIn(['mutes', 'new', 'account']), notifications: state.getIn(['mutes', 'new', 'notifications']), muteDuration: state.getIn(['mutes', 'new', 'duration']), + timelinesOnly: state.getIn(['mutes', 'new', 'timelines_only']), }; }; const mapDispatchToProps = dispatch => { return { - onConfirm(account, notifications, muteDuration) { - dispatch(muteAccount(account.get('id'), notifications, muteDuration)); + onConfirm(account, notifications, timelinesOnly, muteDuration) { + dispatch(muteAccount(account.get('id'), notifications, timelinesOnly, muteDuration)); }, onClose() { @@ -40,6 +42,10 @@ const mapDispatchToProps = dispatch => { onChangeMuteDuration(e) { dispatch(changeMuteDuration(e.target.value)); }, + + onToggleTimelinesOnly() { + dispatch(toggleTimelinesOnly()); + }, }; }; @@ -50,9 +56,11 @@ class MuteModal extends React.PureComponent { static propTypes = { account: PropTypes.object.isRequired, notifications: PropTypes.bool.isRequired, + timelinesOnly: PropTypes.bool.isRequired, onClose: PropTypes.func.isRequired, onConfirm: PropTypes.func.isRequired, onToggleNotifications: PropTypes.func.isRequired, + onTimelinesOnly: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, muteDuration: PropTypes.number.isRequired, onChangeMuteDuration: PropTypes.func.isRequired, @@ -64,7 +72,7 @@ class MuteModal extends React.PureComponent { handleClick = () => { this.props.onClose(); - this.props.onConfirm(this.props.account, this.props.notifications, this.props.muteDuration); + this.props.onConfirm(this.props.account, this.props.notifications, this.props.timelinesOnly, this.props.muteDuration); } handleCancel = () => { @@ -83,8 +91,12 @@ class MuteModal extends React.PureComponent { this.props.onChangeMuteDuration(e); } + toggleTimelinesOnly = () => { + this.props.onToggleTimelinesOnly(); + } + render () { - const { account, notifications, muteDuration, intl } = this.props; + const { account, notifications, timelinesOnly, muteDuration, intl } = this.props; return ( <div className='modal-root__modal mute-modal'> @@ -109,6 +121,13 @@ class MuteModal extends React.PureComponent { </label> </div> <div> + <label htmlFor='mute-modal__timelines-only-checkbox'> + <FormattedMessage id='mute_modal.timelines_only' defaultMessage='Hide from timelines only?' /> + {' '} + <Toggle id='mute-modal__timelines-only-checkbox' checked={timelinesOnly} onChange={this.toggleTimelinesOnly} /> + </label> + </div> + <div> <span><FormattedMessage id='mute_modal.duration' defaultMessage='Duration' />: </span> {/* eslint-disable-next-line jsx-a11y/no-onchange */} diff --git a/app/javascript/flavours/glitch/features/ui/components/report_modal.js b/app/javascript/flavours/glitch/features/ui/components/report_modal.js index 9016b08d7..7473cfbe0 100644 --- a/app/javascript/flavours/glitch/features/ui/components/report_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/report_modal.js @@ -30,7 +30,7 @@ const makeMapStateToProps = () => { account: getAccount(state, accountId), comment: state.getIn(['reports', 'new', 'comment']), forward: state.getIn(['reports', 'new', 'forward']), - statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}:with_replies`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])), + statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}:replies`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])), }; }; @@ -70,12 +70,12 @@ class ReportModal extends ImmutablePureComponent { } componentDidMount () { - this.props.dispatch(expandAccountTimeline(this.props.account.get('id'), { withReplies: true })); + this.props.dispatch(expandAccountTimeline(this.props.account.get('id'), { filter: ':replies' })); } componentWillReceiveProps (nextProps) { if (this.props.account !== nextProps.account && nextProps.account) { - this.props.dispatch(expandAccountTimeline(nextProps.account.get('id'), { withReplies: true })); + this.props.dispatch(expandAccountTimeline(nextProps.account.get('id'), { filter: ':replies' })); } } diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index 61a34fd2b..fa18f84b3 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -215,8 +215,10 @@ class SwitchingColumnsArea extends React.PureComponent { <WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} /> <WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} /> - <WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} /> - <WrappedRoute path='/accounts/:accountId/with_replies' component={AccountTimeline} content={children} componentParams={{ withReplies: true }} /> + <WrappedRoute path='/accounts/:accountId' exact component={AccountTimeline} content={children} componentParams={{ filter: '' }} /> + <WrappedRoute path='/accounts/:accountId/mentions' component={AccountTimeline} content={children} componentParams={{ filter: ':mentions' }} /> + <WrappedRoute path='/accounts/:accountId/with_replies' component={AccountTimeline} content={children} componentParams={{ filter: ':replies' }} /> + <WrappedRoute path='/accounts/:accountId/reblogs' component={AccountTimeline} content={children} componentParams={{ filter: ':reblogs' }} /> <WrappedRoute path='/accounts/:accountId/followers' component={Followers} content={children} /> <WrappedRoute path='/accounts/:accountId/following' component={Following} content={children} /> <WrappedRoute path='/accounts/:accountId/media' component={AccountGallery} content={children} /> diff --git a/app/javascript/flavours/glitch/locales/en-MP.js b/app/javascript/flavours/glitch/locales/en-MP.js new file mode 100644 index 000000000..a84552467 --- /dev/null +++ b/app/javascript/flavours/glitch/locales/en-MP.js @@ -0,0 +1,4 @@ +import messages from 'flavours/glitch/locales/en'; +import messages_mp from 'mastodon/locales/en-MP.json'; + +export default Object.assign({}, messages, messages_mp); \ No newline at end of file diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index e081c31ad..e0ab9f9ab 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -66,6 +66,7 @@ const initialState = ImmutableMap({ do_not_federate: false, threaded_mode: false, }), + id: null, sensitive: false, elefriend: Math.random() < glitchProbability ? Math.floor(Math.random() * totalElefriends) : totalElefriends, spoiler: false, @@ -149,6 +150,7 @@ function apiStatusToTextHashtags (state, status) { function clearAll(state) { return state.withMutations(map => { + map.set('id', null); map.set('text', ''); if (defaultContentType) map.set('content_type', defaultContentType); map.set('spoiler', false); @@ -286,7 +288,9 @@ const expandMentions = status => { const fragment = domParser.parseFromString(status.get('content'), 'text/html').documentElement; status.get('mentions').forEach(mention => { - fragment.querySelector(`a[href="${mention.get('url')}"]`).textContent = `@${mention.get('acct')}`; + const selection = fragment.querySelector(`a[href="${mention.get('url')}"]`); + if (!selection) return; + selection.textContent = `@${mention.get('acct')}`; }); return fragment.innerHTML; @@ -403,9 +407,14 @@ export default function compose(state = initialState, action) { } }); case COMPOSE_REPLY_CANCEL: - state = state.setIn(['advanced_options', 'threaded_mode'], false); + return state.withMutations(map => { + map.set('id', null); + map.set('in_reply_to', null); + map.set('idempotencyKey', uuid()); + }); case COMPOSE_RESET: return state.withMutations(map => { + map.set('id', null); map.set('in_reply_to', null); if (defaultContentType) map.set('content_type', defaultContentType); map.set('text', ''); @@ -505,6 +514,7 @@ export default function compose(state = initialState, action) { let text = action.raw_text || unescapeHTML(expandMentions(action.status)); if (do_not_federate) text = text.replace(/ ?👁\ufe0f?\u200b?$/, ''); return state.withMutations(map => { + map.set('id', action.inplace ? action.status.get('id') : null); map.set('text', text); map.set('content_type', action.content_type || 'text/plain'); map.set('in_reply_to', action.status.get('in_reply_to_id')); diff --git a/app/javascript/flavours/glitch/reducers/local_settings.js b/app/javascript/flavours/glitch/reducers/local_settings.js index ea37ae4aa..53360b8f0 100644 --- a/app/javascript/flavours/glitch/reducers/local_settings.js +++ b/app/javascript/flavours/glitch/reducers/local_settings.js @@ -10,18 +10,18 @@ const initialState = ImmutableMap({ stretch : true, navbar_under : false, swipe_to_change_columns: true, - side_arm : 'none', - side_arm_reply_mode : 'keep', - show_reply_count : false, - always_show_spoilers_field: false, - confirm_missing_media_description: false, + side_arm : 'private', + side_arm_reply_mode : 'restrict', + show_reply_count : true, + always_show_spoilers_field: true, + confirm_missing_media_description: true, confirm_boost_missing_media_description: false, confirm_before_clearing_draft: true, prepend_cw_re: true, preselect_on_reply: true, inline_preview_cards: true, - hicolor_privacy_icons: false, - show_content_type_choice: false, + hicolor_privacy_icons: true, + show_content_type_choice: true, filtering_behavior: 'hide', tag_misleading_links: true, rewrite_mentions: 'no', @@ -53,7 +53,7 @@ const initialState = ImmutableMap({ pop_in_position : 'right', }), notifications : ImmutableMap({ - favicon_badge : false, + favicon_badge : true, tab_badge : true, show_unread : true, }), diff --git a/app/javascript/flavours/glitch/reducers/mutes.js b/app/javascript/flavours/glitch/reducers/mutes.js index d346d9a78..f116e106a 100644 --- a/app/javascript/flavours/glitch/reducers/mutes.js +++ b/app/javascript/flavours/glitch/reducers/mutes.js @@ -4,6 +4,7 @@ import { MUTES_INIT_MODAL, MUTES_TOGGLE_HIDE_NOTIFICATIONS, MUTES_CHANGE_DURATION, + MUTES_TOGGLE_TIMELINES_ONLY, } from 'flavours/glitch/actions/mutes'; const initialState = Immutable.Map({ @@ -11,6 +12,7 @@ const initialState = Immutable.Map({ account: null, notifications: true, duration: 0, + timelinesOnly: false, }), }); @@ -20,11 +22,14 @@ export default function mutes(state = initialState, action) { return state.withMutations((state) => { state.setIn(['new', 'account'], action.account); state.setIn(['new', 'notifications'], true); + state.setIn(['new', 'timelinesOnly'], false); }); case MUTES_TOGGLE_HIDE_NOTIFICATIONS: return state.updateIn(['new', 'notifications'], (old) => !old); case MUTES_CHANGE_DURATION: return state.setIn(['new', 'duration'], Number(action.duration)); + case MUTES_TOGGLE_TIMELINES_ONLY: + return state.updateIn(['new', 'timelines_only'], (old) => !old); default: return state; } diff --git a/app/javascript/flavours/glitch/reducers/settings.js b/app/javascript/flavours/glitch/reducers/settings.js index bf0545c48..64d13e18c 100644 --- a/app/javascript/flavours/glitch/reducers/settings.js +++ b/app/javascript/flavours/glitch/reducers/settings.js @@ -91,8 +91,9 @@ const initialState = ImmutableMap({ const defaultColumns = fromJS([ { id: 'COMPOSE', uuid: uuid(), params: {} }, - { id: 'HOME', uuid: uuid(), params: {} }, { id: 'NOTIFICATIONS', uuid: uuid(), params: {} }, + { id: 'HOME', uuid: uuid(), params: {} }, + { id: 'COMMUNITY', uuid: uuid(), params: {} }, ]); const hydrate = (state, settings) => state.mergeDeep(settings).update('columns', (val = defaultColumns) => val); diff --git a/app/javascript/flavours/glitch/reducers/statuses.js b/app/javascript/flavours/glitch/reducers/statuses.js index 5db766b96..20822b4cb 100644 --- a/app/javascript/flavours/glitch/reducers/statuses.js +++ b/app/javascript/flavours/glitch/reducers/statuses.js @@ -10,6 +10,7 @@ import { import { STATUS_MUTE_SUCCESS, STATUS_UNMUTE_SUCCESS, + STATUS_PUBLISH_SUCCESS, } from 'flavours/glitch/actions/statuses'; import { TIMELINE_DELETE, @@ -56,6 +57,8 @@ export default function statuses(state = initialState, action) { return state.setIn([action.id, 'muted'], true); case STATUS_UNMUTE_SUCCESS: return state.setIn([action.id, 'muted'], false); + case STATUS_PUBLISH_SUCCESS: + return state.setIn([action.id, 'published'], true); case TIMELINE_DELETE: return deleteStatus(state, action.id, action.references); default: diff --git a/app/javascript/flavours/glitch/selectors/index.js b/app/javascript/flavours/glitch/selectors/index.js index bb9180d12..3571aea3e 100644 --- a/app/javascript/flavours/glitch/selectors/index.js +++ b/app/javascript/flavours/glitch/selectors/index.js @@ -141,6 +141,11 @@ export const makeGetStatus = () => { } } + if (statusReblog) { + statusReblog = statusReblog.set('reblogSpoilerPresent', statusBase.get('spoiler_text').length > 0); + statusReblog = statusReblog.set('reblogSpoilerHtml', statusBase.get('spoilerHtml')); + } + return statusBase.withMutations(map => { map.set('reblog', statusReblog); map.set('account', accountBase); diff --git a/app/javascript/flavours/glitch/styles/components/columns.scss b/app/javascript/flavours/glitch/styles/components/columns.scss index 2550f50f4..d90327211 100644 --- a/app/javascript/flavours/glitch/styles/components/columns.scss +++ b/app/javascript/flavours/glitch/styles/components/columns.scss @@ -696,12 +696,9 @@ } .notifications-permission-banner { - padding: 30px; + padding: 14px; border-bottom: 1px solid lighten($ui-base-color, 8%); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; + text-align: center; position: relative; &__close { @@ -711,7 +708,7 @@ } h2 { - font-size: 16px; + font-size: 14px; font-weight: 500; margin-bottom: 15px; text-align: center; @@ -722,4 +719,9 @@ margin-bottom: 15px; text-align: center; } + + .button { + margin-left: 7px; + margin-right: 7px; + } } diff --git a/app/javascript/flavours/glitch/styles/components/composer.scss b/app/javascript/flavours/glitch/styles/components/composer.scss index 460f75c1f..7db2dd2aa 100644 --- a/app/javascript/flavours/glitch/styles/components/composer.scss +++ b/app/javascript/flavours/glitch/styles/components/composer.scss @@ -654,6 +654,19 @@ text-align: center; } + & > .privacy_warning { + background-color: $error-value-color; + + &:hover { + background-color: lighten($error-value-color, 5%); + } + + &:active, + &:focus { + background-color: darken($error-value-color, 5%); + } + } + &.over { & > .count { color: $warning-red } } diff --git a/app/javascript/flavours/glitch/styles/components/modal.scss b/app/javascript/flavours/glitch/styles/components/modal.scss index bc0965864..d9d962e34 100644 --- a/app/javascript/flavours/glitch/styles/components/modal.scss +++ b/app/javascript/flavours/glitch/styles/components/modal.scss @@ -876,7 +876,7 @@ width: 100%; border: none; padding: 10px; - font-family: 'mastodon-font-monospace', monospace; + font-family: 'roboto-mono', monospace; background: $ui-base-color; color: $primary-text-color; font-size: 14px; diff --git a/app/javascript/flavours/glitch/styles/containers.scss b/app/javascript/flavours/glitch/styles/containers.scss index d1c6c33d7..eab6e480c 100644 --- a/app/javascript/flavours/glitch/styles/containers.scss +++ b/app/javascript/flavours/glitch/styles/containers.scss @@ -208,6 +208,18 @@ margin-bottom: 10px; } + @media screen and (max-width: 800px) { + .column-3 { + grid-column: 3 / 5; + grid-row: 3; + } + + .column-4 { + grid-column: 1/3; + grid-row: 3; + } + } + @media screen and (max-width: 738px) { grid-template-columns: minmax(0, 50%) minmax(0, 50%); @@ -656,7 +668,7 @@ box-sizing: border-box; flex: 0 0 auto; color: $darker-text-color; - padding: 10px; + margin: 15px 0px; border-right: 1px solid lighten($ui-base-color, 4%); cursor: default; text-align: center; @@ -707,6 +719,7 @@ .counter-label { font-size: 12px; + font-weight: bold; display: block; } diff --git a/app/javascript/flavours/glitch/styles/index.scss b/app/javascript/flavours/glitch/styles/index.scss index af73feb89..c1ed4a6f1 100644 --- a/app/javascript/flavours/glitch/styles/index.scss +++ b/app/javascript/flavours/glitch/styles/index.scss @@ -1,6 +1,7 @@ @import 'mixins'; @import 'variables'; @import 'styles/fonts/roboto'; +@import 'styles/fonts/opensans'; @import 'styles/fonts/roboto-mono'; @import 'styles/fonts/montserrat'; @@ -23,3 +24,5 @@ @import 'accessibility'; @import 'rtl'; @import 'dashboard'; + +@import 'monsterfork/index'; diff --git a/app/javascript/flavours/glitch/styles/monsterfork/about.scss b/app/javascript/flavours/glitch/styles/monsterfork/about.scss new file mode 100644 index 000000000..4ab9cfa7c --- /dev/null +++ b/app/javascript/flavours/glitch/styles/monsterfork/about.scss @@ -0,0 +1,9 @@ +.box-widget { + .simple_form p.lead { + color: $darker-text-color; + font-size: 15px; + line-height: 20px; + font-weight: bold; + margin-bottom: 25px; + } +} \ No newline at end of file diff --git a/app/javascript/flavours/glitch/styles/monsterfork/components/composer.scss b/app/javascript/flavours/glitch/styles/monsterfork/components/composer.scss new file mode 100644 index 000000000..ba347b1cc --- /dev/null +++ b/app/javascript/flavours/glitch/styles/monsterfork/components/composer.scss @@ -0,0 +1,11 @@ +.composer--publisher { + .clear { + background: darken($ui-base-color, 8%); + color: $secondary-text-color; + margin: 0 2px; + padding: 0; + width: 36px; + text-align: center; + float: left; + } +} diff --git a/app/javascript/flavours/glitch/styles/monsterfork/components/formatting.scss b/app/javascript/flavours/glitch/styles/monsterfork/components/formatting.scss new file mode 100644 index 000000000..44df7efc9 --- /dev/null +++ b/app/javascript/flavours/glitch/styles/monsterfork/components/formatting.scss @@ -0,0 +1,175 @@ +.status__content__text, +.reply-indicator__content, +.composer--reply > .content, +.account__header__content, +.status__content > .e-content +{ + .emojione { + width: 20px; + height: 20px; + margin: -3px 0 0; + } + + & > ul, + & > ol { + margin-bottom: 20px; + } + + h1, h2, h3, h4, h5 { + margin-top: 20px; + margin-bottom: 20px; + } + + h1, h2 { + font-weight: 700; + font-size: 1.2em; + } + + h2 { + font-size: 1.1em; + } + + h3, h4, h5 { + font-weight: 500; + } + + blockquote { + padding-left: 10px; + border-left: 3px solid $darker-text-color; + color: $darker-text-color; + white-space: normal; + + p:last-child { + margin-bottom: 0; + } + } + + b, strong { + font-weight: 700; + } + + em, i { + font-style: italic; + } + + sub { + font-size: smaller; + text-align: sub; + } + + sup { + font-size: smaller; + vertical-align: super; + } + + ul, ol { + margin-left: 1em; + + p { + margin: 0; + } + } + + ul { + list-style-type: disc; + } + + ol { + list-style-type: decimal; + } + + a { + color: $secondary-text-color; + text-decoration: none; + + &:hover { + text-decoration: underline; + + .fa { + color: lighten($dark-text-color, 7%); + } + } + + &.mention { + &:hover { + text-decoration: underline; + + span { + text-decoration: none; + } + } + } + + .fa { + color: $dark-text-color; + } + } + + a.unhandled-link { + color: lighten($ui-highlight-color, 8%); + + .link-origin-tag { + color: $gold-star; + font-size: 0.8em; + } + } + + s { text-decoration: line-through; } + del { text-decoration: line-through; } + h6 { font-size: 8px; font-weight: bold; } + hr { border-color: lighten($dark-text-color, 10%); } + pre, code { + color: #6c6; + text-shadow: 0 0 4px #0f0; + + background: linear-gradient( + to bottom, + #121 1px, + #232 1px + ); + background-size: 100% 2px; + } + pre { + & > code { + background: transparent; + } + padding: 10px; + border: 2px solid darken($ui-base-color, 20%); + } + mark { + background-color: #ccff15; + color: black; + } + blockquote { + font-style: italic; + } + .center, .centered, center { + text-align: center; + } + summary { + color: lighten($primary-text-color, 33%); + font-weight: bold; + + &:focus, &:active { + outline: none; + } + } + details > p, details > span { + padding-top: 5px; + padding-left: 10px; + border-left: 3px solid $darker-text-color; + color: $darker-text-color; + white-space: normal; + + p:last-child { + margin-bottom: 0; + }; + } + p[data-name="footer"] { + color: lighten($dark-text-color, 10%); + font-style: italic; + font-size: 12px; + text-align: right; + margin-top: 0px; + } +} \ No newline at end of file diff --git a/app/javascript/flavours/glitch/styles/monsterfork/components/index.scss b/app/javascript/flavours/glitch/styles/monsterfork/components/index.scss new file mode 100644 index 000000000..84da74f82 --- /dev/null +++ b/app/javascript/flavours/glitch/styles/monsterfork/components/index.scss @@ -0,0 +1,3 @@ +@import 'composer'; +@import 'status'; +@import 'formatting'; diff --git a/app/javascript/flavours/glitch/styles/monsterfork/components/status.scss b/app/javascript/flavours/glitch/styles/monsterfork/components/status.scss new file mode 100644 index 000000000..1d2f053c0 --- /dev/null +++ b/app/javascript/flavours/glitch/styles/monsterfork/components/status.scss @@ -0,0 +1,243 @@ +.status__notice-wrapper:empty, +.status__footers:empty { + display: none; +} + +.status__notice { + display: flex; + align-items: center; + + & > span, & > a { + display: inline-flex; + align-items: center; + line-height: normal; + font-style: italic; + font-weight: bold; + font-size: 12px; + padding-left: 8px; + height: 1.5em; + } + + & > span { + color: $dark-text-color; + + & > time:before { + content: " "; + white-space: pre; + } + } + + & > i { + display: inline-flex; + align-items: center; + color: lighten($dark-text-color, 4%); + width: 1.1em; + height: 1.5em; + } +} + +.status__footers { + font-size: 12px; + margin-top: 1em; + + & > details { + & > summary { + &:focus, &:active { + outline: none; + } + } + + & > summary > span, + & > ul > li > span, + & > ul > li > a { + color: lighten($dark-text-color, 4%); + padding-left: 8px; + } + } + + .status__tags { + & > ul { + display: flex; + flex-direction: row; + flex-wrap: wrap; + } + + & > ul > li { + list-style: none; + display: inline-block; + width: 50%; + } + + & > summary > i, + & > ul > li > i { + color: #669999; + } + } + + .status__permissions { + & > summary > i { + color: #999966; + } + + & > ul > li { + &.permission-status > i { + color: #99cccc; + } + + &.permission-account > i { + color: #cc99cc; + } + + & > span { + & > span, & > code { + color: lighten($primary-text-color, 30%); + } + + & > span:first-child { + display: inline-block; + text-transform: capitalize; + min-width: 5em; + } + } + } + } +} + +.status, .detailed-status { + &.unpublished { + background: darken($ui-base-color, 4%); + + &:focus { + background: lighten($ui-base-color, 4%); + } + } + + &[data-local-only="true"] { + background: lighten($ui-base-color, 4%); + } +} + +div[data-nest-level] { + border-style: solid; +} + +@for $i from 0 through 15 { + div[data-nest-level="#{$i}"] { + border-left-width: #{$i * 3}px; + border-left-color: darken($ui-base-color, 8%); + } +} + +div[data-nest-deep="true"] { + border-left-width: 75px; + border-left-color: darken($ui-base-color, 8%); +} + +.status__content { + .status__content__text, + .e-content { + img:not(.emojione) { + max-width: 100%; + margin: 1em auto; + } + } + + p:first-child, + pre:first-child, + blockquote:first-child, + div.status__notice-wrapper + p { + margin-top: 0px; + } + + p, pre, blockquote { + margin-top: 1em; + margin-bottom: 0px; + } + + .status__content__spoiler--visible { + margin-top: 1em; + margin-bottom: 1em; + } + + .spoiler { + & > i { + width: 1.1em; + color: lighten($dark-text-color, 4%); + } + + & > span { + padding-left: 8px; + } + } + + .reblog-spoiler { + font-style: italic; + + & > span { + color: lighten($ui-highlight-color, 8%); + } + } +} + +div.media-caption { + background: $ui-base-color; + + strong { + font-weight: bold; + } + + p { + font-size: 12px !important; + padding: 0px 10px; + text-align: center; + } + a { + color: $secondary-text-color; + text-decoration: none; + font-weight: bold; + + &:hover { + text-decoration: underline; + + .fa { + color: lighten($dark-text-color, 7%); + } + } + + &.mention { + &:hover { + text-decoration: none; + + span { + text-decoration: underline; + } + } + } + + .fa { + color: $dark-text-color; + } + } +} + +.status__prepend { + margin-left: 0px; + + .status__prepend-icon-wrapper { + left: 4px; + } + + & > span { + margin-left: 25px; + } +} + +.embed .status__prepend, +.public-layout .status__prepend { + margin: -10px 0px 0px 5px; +} + +.public-layout .status__prepend-icon-wrapper { + left: unset; + right: 4px; +} \ No newline at end of file diff --git a/app/javascript/flavours/glitch/styles/monsterfork/index.scss b/app/javascript/flavours/glitch/styles/monsterfork/index.scss new file mode 100644 index 000000000..9888adfe4 --- /dev/null +++ b/app/javascript/flavours/glitch/styles/monsterfork/index.scss @@ -0,0 +1,2 @@ +@import 'components/index'; +@import 'about'; \ No newline at end of file diff --git a/app/javascript/flavours/glitch/styles/nightshade.scss b/app/javascript/flavours/glitch/styles/nightshade.scss new file mode 100644 index 000000000..bc8069e59 --- /dev/null +++ b/app/javascript/flavours/glitch/styles/nightshade.scss @@ -0,0 +1,3 @@ +@import 'nightshade/variables'; +@import 'index'; +@import 'nightshade/diff'; diff --git a/app/javascript/flavours/glitch/styles/nightshade/diff.scss b/app/javascript/flavours/glitch/styles/nightshade/diff.scss new file mode 100644 index 000000000..de1278114 --- /dev/null +++ b/app/javascript/flavours/glitch/styles/nightshade/diff.scss @@ -0,0 +1,440 @@ +// Notes! +// Sass color functions, "darken" and "lighten" are automatically replaced. + +.glitch.local-settings { + background: darken($ui-base-color, 80%); + + &__navigation { + background: darken($ui-base-color, 30%); + } + + &__navigation__item { + background: darken($ui-base-color, 50%); + + &:hover { + background: $ui-base-color; + color: $primary-text-color; + } + } +} + +.notification__dismiss-overlay { + .wrappy { + box-shadow: unset; + } + + .ckbox { + text-shadow: unset; + } +} + +.status.status-direct:not(.read) { + background: darken($ui-base-color, 8%); + border-bottom-color: darken($ui-base-color, 12%); + + &.collapsed> .status__content:after { + background: linear-gradient(rgba(darken($ui-base-color, 8%), 0), rgba(darken($ui-base-color, 8%), 1)); + } +} + +.focusable:focus.status.status-direct:not(.read) { + background: darken($ui-base-color, 4%); + + &.collapsed> .status__content:after { + background: linear-gradient(rgba(darken($ui-base-color, 4%), 0), rgba(darken($ui-base-color, 4%), 1)); + } +} + +// Change columns' default background colors +.column { + > .scrollable { + background: darken($ui-base-color, 13%); + } +} + +.status.collapsed .status__content:after { + background: linear-gradient(rgba(darken($ui-base-color, 13%), 0), rgba(darken($ui-base-color, 13%), 1)); +} + +.drawer__inner { + background: $ui-base-color; +} + +.drawer__inner__mastodon { + background: $ui-base-color url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color(darken($ui-base-color, 13%))}"/></svg>') no-repeat bottom / 100% auto !important; + + .mastodon { + filter: contrast(75%) brightness(75%) !important; + } +} + +// Change the default appearance of the content warning button +.status__content { + + .status__content__spoiler-link { + + background: darken($ui-base-color, 30%); + + &:hover { + background: lighten($ui-base-color, 35%); + color: $primary-text-color; + text-decoration: none; + } + + } + +} + +// Change the background colors of media and video spoilers +.media-spoiler, +.video-player__spoiler, +.account-gallery__item a { + background: $ui-base-color; +} + +// Change the colors used in the dropdown menu +.dropdown-menu { + background: $ui-base-color; +} + +.dropdown-menu__arrow { + + &.left { + border-left-color: $ui-base-color; + } + + &.top { + border-top-color: $ui-base-color; + } + + &.bottom { + border-bottom-color: $ui-base-color; + } + + &.right { + border-right-color: $ui-base-color; + } + +} + +.dropdown-menu__item { + a { + background: $ui-base-color; + color: $ui-secondary-color; + } +} + +// Change the default color of several parts of the compose form +.composer { + + .composer--spoiler input, .compose-form__autosuggest-wrapper textarea { + color: lighten($ui-base-color, 80%); + + &:disabled { background: lighten($simple-background-color, 10%) } + + &::placeholder { + color: lighten($ui-base-color, 70%); + } + } + + .compose-form__modifiers { + background: darken($ui-base-color, 60%); + + .autosuggest-input input, select { + background: darken($ui-base-color, 70%); + } + } + + .composer--options-wrapper { + background: lighten($ui-base-color, 10%); + } + + .composer--options > hr { + display: none; + } + + .composer--options--dropdown--content--item { + color: $ui-primary-color; + + strong { + color: $ui-primary-color; + } + + } + + header > .account.small { + color: $primary-text-color; + } + + .composer--reply > .content { + color: $primary-text-color; + } +} + +.composer--upload_form--actions .icon-button { + color: lighten($white, 7%); + + &:active, + &:focus, + &:hover { + color: $white; + } +} + +.composer--upload_form--item > div input { + color: lighten($white, 7%); + + &::placeholder { + color: lighten($white, 10%); + } +} + +.dropdown-menu__separator { + border-bottom-color: lighten($ui-base-color, 12%); +} + +.status__content, +.reply-indicator__content { + a { + color: $highlight-text-color; + } +} + +.emoji-mart-bar { + border-color: darken($ui-base-color, 4%); + + &:first-child { + background: lighten($ui-base-color, 10%); + } +} + +.emoji-mart-search input { + background: rgba($ui-base-color, 0.3); + border-color: $ui-base-color; +} + +.autosuggest-textarea__suggestions { + background: darken($ui-base-color, 40%) +} + +.autosuggest-textarea__suggestions__item { + &:hover, + &:focus, + &:active, + &.selected { + background: darken($ui-base-color, 4%); + color: $primary-text-color; + } +} + +.react-toggle-track { + background: $ui-secondary-color; +} + +.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track { + background: lighten($ui-secondary-color, 10%); +} + +.react-toggle.react-toggle--checked:hover:not(.react-toggle--disabled) .react-toggle-track { + background: darken($ui-highlight-color, 10%); +} + +// Change the background colors of modals +.actions-modal, +.boost-modal, +.favourite-modal, +.confirmation-modal, +.mute-modal, +.block-modal, +.report-modal, +.embed-modal, +.error-modal, +.onboarding-modal, +.report-modal__comment .setting-text__wrapper, +.report-modal__comment .setting-text { + background: $primary-text-color; + border: 1px solid lighten($ui-base-color, 8%); +} + +.report-modal__comment { + border-right-color: lighten($ui-base-color, 8%); +} + +.report-modal__container { + border-top-color: lighten($ui-base-color, 8%); +} + +.boost-modal__action-bar, +.confirmation-modal__action-bar, +.mute-modal__action-bar, +.block-modal__action-bar, +.onboarding-modal__paginator, +.error-modal__footer { + background: darken($ui-base-color, 20%); + + .onboarding-modal__nav, + .error-modal__nav { + &:hover, + &:focus, + &:active { + background-color: darken($ui-base-color, 12%); + } + } +} + +// Change the default color used for the text in an empty column or on the error column +.empty-column-indicator, +.error-column { + color: darken($ui-base-color, 60%); +} + +// Change the default colors used on some parts of the profile pages +.activity-stream-tabs { + + background: $account-background-color; + + a { + &.active { + color: $ui-primary-color; + } + } + +} + +.activity-stream { + + .entry { + background: $account-background-color; + } + + .status.light { + + .status__content { + color: $primary-text-color; + } + + .display-name { + strong { + color: $primary-text-color; + } + } + + } + +} + +.accounts-grid { + .account-grid-card { + + .controls { + .icon-button { + color: $ui-secondary-color; + } + } + + .name { + a { + color: $primary-text-color; + } + } + + .username { + color: $ui-secondary-color; + } + + .account__header__content { + color: $primary-text-color; + } + + } +} + +.button.logo-button { + color: $white; + + svg { + fill: $white; + } +} + +.public-layout { + .header, + .public-account-header, + .public-account-bio { + box-shadow: none; + } + + .header { + background: lighten($ui-base-color, 12%); + } + + .public-account-header { + &__image { + background: lighten($ui-base-color, 12%); + + &::after { + box-shadow: none; + } + } + + &__tabs { + &__name { + h1, + h1 small { + color: $white; + } + } + } + } +} + +.account__section-headline a.active::after { + border-color: transparent transparent $white; +} + +.hero-widget, +.box-widget, +.contact-widget, +.landing-page__information.contact-widget, +.moved-account-widget, +.memoriam-widget, +.activity-stream, +.nothing-here, +.directory__tag > a, +.directory__tag > div { + box-shadow: none; +} + +.admin-wrapper { + .sidebar ul .simple-navigation-active-leaf a { + color: $black; + } +} + +.simple_form button, .button { + color: $black; +} + +.poll__input { + border: 1px solid pink; +} + +.poll .button.button-secondary { + background: $primary-text-color; + color: $black; +} + +button.icon-button { + color: $ui-secondary-color; +} + +button.icon-button i.fa-retweet { + background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="22" height="209"><path d="M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z" fill="#{hex-color($ui-secondary-color)}" stroke-width="0"/><path d="M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z" fill="#{hex-color($ui-secondary-color)}" stroke-width="0"/></svg>'); +} + +button.icon-button.active i.fa-retweet { + background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="22" height="209"><path d="M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z" fill="pink" stroke-width="0"/><path d="M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z" fill="pink" stroke-width="0"/></svg>'); + box-shadow: 0px 0px 5px pink, inset 0px 0px 5px pink; + border-radius: 20px; +} + diff --git a/app/javascript/flavours/glitch/styles/nightshade/variables.scss b/app/javascript/flavours/glitch/styles/nightshade/variables.scss new file mode 100644 index 000000000..46f055a8f --- /dev/null +++ b/app/javascript/flavours/glitch/styles/nightshade/variables.scss @@ -0,0 +1,41 @@ +// Dependent colors +$black: #000000; +$white: #ffffff; + +$classic-base-color: #c8b7c1; +$classic-primary-color: #4C3A45; +$classic-secondary-color: #2C2028; +$classic-highlight-color: #bca9b4; + +$ui-base-color: $classic-secondary-color !default; +$ui-base-lighter-color: darken($ui-base-color, 57%); +$ui-highlight-color: $classic-highlight-color !default; +$ui-primary-color: $classic-primary-color !default; +$ui-secondary-color: $classic-base-color !default; + +$primary-text-color: #e9e2e6 !default; +$darker-text-color: $classic-base-color !default; +$dark-text-color: #a68c9c; +$action-button-color: #606984; + +$success-green: #80b38b; +$error-red: #b38080; +$warning-red: #b38c80; + +$base-overlay-background: $black !default; + +$inverted-text-color: #291822 !default; +$lighter-text-color: $classic-base-color !default; +$light-text-color: #6A5160; + +$account-background-color: #4C3A45 !default; + +@function darken($color, $amount) { + @return hsl(hue($color), saturation($color), lightness($color) + $amount); +} + +@function lighten($color, $amount) { + @return hsl(hue($color), saturation($color), lightness($color) - $amount); +} + +//$emojis-requiring-inversion: 'chains'; diff --git a/app/javascript/flavours/glitch/styles/variables.scss b/app/javascript/flavours/glitch/styles/variables.scss index 6e242281b..f4e971996 100644 --- a/app/javascript/flavours/glitch/styles/variables.scss +++ b/app/javascript/flavours/glitch/styles/variables.scss @@ -51,11 +51,11 @@ $media-modal-media-max-width: 100%; // put margins on top and bottom of image to avoid the screen covered by image. $media-modal-media-max-height: 80%; -$no-gap-breakpoint: 415px; +$no-gap-breakpoint: 700px; -$font-sans-serif: 'mastodon-font-sans-serif' !default; -$font-display: 'mastodon-font-display' !default; -$font-monospace: 'mastodon-font-monospace' !default; +$font-sans-serif: 'opensans' !default; +$font-display: 'montserrat' !default; +$font-monospace: 'roboto-mono' !default; // Avatar border size (8% default, 100% for rounded avatars) $ui-avatar-border-size: 8%; diff --git a/app/javascript/flavours/glitch/styles/widgets.scss b/app/javascript/flavours/glitch/styles/widgets.scss index b397844a2..d2f4450fc 100644 --- a/app/javascript/flavours/glitch/styles/widgets.scss +++ b/app/javascript/flavours/glitch/styles/widgets.scss @@ -576,7 +576,6 @@ $fluid-breakpoint: $maximum-width + 20px; .table-of-contents { background: darken($ui-base-color, 4%); - min-height: 100%; font-size: 14px; border-radius: 4px; diff --git a/app/javascript/flavours/glitch/util/initial_state.js b/app/javascript/flavours/glitch/util/initial_state.js index 911468e6f..05868970c 100644 --- a/app/javascript/flavours/glitch/util/initial_state.js +++ b/app/javascript/flavours/glitch/util/initial_state.js @@ -11,6 +11,8 @@ const initialState = element && function () { const getMeta = (prop) => initialState && initialState.meta && initialState.meta[prop]; +export const webPushEnabled = getMeta('web_push') === true; + export const reduceMotion = getMeta('reduce_motion'); export const autoPlayGif = getMeta('auto_play_gif'); export const displaySensitiveMedia = getMeta('display_sensitive_media'); |