From 383136d9bb290ebca7d8414b151c14d6038d5bc3 Mon Sep 17 00:00:00 2001 From: PatOnTheBack <51241310+PatOnTheBack@users.noreply.github.com> Date: Tue, 25 Jun 2019 20:16:24 -0400 Subject: [Glitch] Removed extra pipes from regex. Port 5b20284f6f7ebb2514d81fb27fcaa40f8edf14ff to glitch-soc Signed-off-by: Thibaut Girka --- app/javascript/flavours/glitch/util/emoji/emoji_mart_search_light.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/javascript/flavours/glitch/util') diff --git a/app/javascript/flavours/glitch/util/emoji/emoji_mart_search_light.js b/app/javascript/flavours/glitch/util/emoji/emoji_mart_search_light.js index 164fdcc0b..e4519a13e 100644 --- a/app/javascript/flavours/glitch/util/emoji/emoji_mart_search_light.js +++ b/app/javascript/flavours/glitch/util/emoji/emoji_mart_search_light.js @@ -74,7 +74,7 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo return [emojisList['-1']]; } - let values = value.toLowerCase().split(/[\s|,|\-|_]+/), + let values = value.toLowerCase().split(/[\s|,\-_]+/), allResults = []; if (values.length > 2) { -- cgit From 4175f131557f3633d38e78692698469c3652e398 Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 26 Jun 2019 19:33:04 +0200 Subject: [Glitch] Add option to disable blurhash previews Port 3086c645fde2345d34e401bdf3e2f19f19da3294 to glitch-soc --- app/javascript/flavours/glitch/components/media_gallery.js | 4 +++- app/javascript/flavours/glitch/features/video/index.js | 4 ++-- app/javascript/flavours/glitch/util/initial_state.js | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) (limited to 'app/javascript/flavours/glitch/util') diff --git a/app/javascript/flavours/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js index 291caff45..04d3ce751 100644 --- a/app/javascript/flavours/glitch/components/media_gallery.js +++ b/app/javascript/flavours/glitch/components/media_gallery.js @@ -6,7 +6,7 @@ import IconButton from './icon_button'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { isIOS } from 'flavours/glitch/util/is_mobile'; import classNames from 'classnames'; -import { autoPlayGif, displayMedia } from 'flavours/glitch/util/initial_state'; +import { autoPlayGif, displayMedia, useBlurhash } from 'flavours/glitch/util/initial_state'; import { decode } from 'blurhash'; const messages = defineMessages({ @@ -101,6 +101,8 @@ class Item extends React.PureComponent { } _decode () { + if (!useBlurhash) return; + const hash = this.props.attachment.get('blurhash'); const pixels = decode(hash, 32, 32); diff --git a/app/javascript/flavours/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js index b73ea0b07..112f9d101 100644 --- a/app/javascript/flavours/glitch/features/video/index.js +++ b/app/javascript/flavours/glitch/features/video/index.js @@ -5,7 +5,7 @@ import { fromJS, is } from 'immutable'; import { throttle } from 'lodash'; import classNames from 'classnames'; import { isFullscreen, requestFullscreen, exitFullscreen } from 'flavours/glitch/util/fullscreen'; -import { displayMedia } from 'flavours/glitch/util/initial_state'; +import { displayMedia, useBlurhash } from 'flavours/glitch/util/initial_state'; import { decode } from 'blurhash'; const messages = defineMessages({ @@ -312,7 +312,7 @@ export default class Video extends React.PureComponent { } _decode () { - if (!this.canvas) return; + if (!this.canvas || !useBlurhash) return; const hash = this.props.blurhash; const pixels = decode(hash, 32, 32); diff --git a/app/javascript/flavours/glitch/util/initial_state.js b/app/javascript/flavours/glitch/util/initial_state.js index f42c06a3a..e8811a6ce 100644 --- a/app/javascript/flavours/glitch/util/initial_state.js +++ b/app/javascript/flavours/glitch/util/initial_state.js @@ -29,5 +29,6 @@ export const mascot = getMeta('mascot'); export const isStaff = getMeta('is_staff'); export const defaultContentType = getMeta('default_content_type'); export const forceSingleColumn = getMeta('advanced_layout') === false; +export const useBlurhash = getMeta('use_blurhash'); export default initialState; -- cgit From 662252c8f77e8dd8fd015bc6ece0943f068b1710 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 28 Jun 2019 15:54:10 +0200 Subject: [Glitch] Add categories for custom emojis Port front-end changes from e64e6a03dd1e0978fee48f0596dcfbc7fd29958f to glitch-soc Signed-off-by: Thibaut Girka --- .../flavours/glitch/features/emoji_picker/index.js | 30 ++++++++++++---------- app/javascript/flavours/glitch/util/emoji/index.js | 3 +++ 2 files changed, 19 insertions(+), 14 deletions(-) (limited to 'app/javascript/flavours/glitch/util') diff --git a/app/javascript/flavours/glitch/features/emoji_picker/index.js b/app/javascript/flavours/glitch/features/emoji_picker/index.js index a78117971..9821502d3 100644 --- a/app/javascript/flavours/glitch/features/emoji_picker/index.js +++ b/app/javascript/flavours/glitch/features/emoji_picker/index.js @@ -11,7 +11,7 @@ import Overlay from 'react-overlays/lib/Overlay'; import classNames from 'classnames'; import ImmutablePropTypes from 'react-immutable-proptypes'; import detectPassiveEvents from 'detect-passive-events'; -import { buildCustomEmojis } from 'flavours/glitch/util/emoji'; +import { buildCustomEmojis, categoriesFromEmojis } from 'flavours/glitch/util/emoji'; const messages = defineMessages({ emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, @@ -110,19 +110,6 @@ let EmojiPicker, Emoji; // load asynchronously const backgroundImageFn = () => `${assetHost}/emoji/sheet_10.png`; const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; -const categoriesSort = [ - 'recent', - 'custom', - 'people', - 'nature', - 'foods', - 'activity', - 'places', - 'objects', - 'symbols', - 'flags', -]; - class ModifierPickerMenu extends React.PureComponent { static propTypes = { @@ -320,8 +307,23 @@ class EmojiPickerMenu extends React.PureComponent { } const title = intl.formatMessage(messages.emoji); + const { modifierOpen } = this.state; + const categoriesSort = [ + 'recent', + 'people', + 'nature', + 'foods', + 'activity', + 'places', + 'objects', + 'symbols', + 'flags', + ]; + + categoriesSort.splice(1, 0, ...Array.from(categoriesFromEmojis(custom_emojis)).sort()); + return (
{ keywords: [name], imageUrl: url, custom: true, + customCategory: emoji.get('category'), }); }); return emojis; }; + +export const categoriesFromEmojis = customEmojis => customEmojis.reduce((set, emoji) => set.add(emoji.get('category') ? `custom-${emoji.get('category')}` : 'custom'), new Set()); -- cgit From 46829e009ee26603e3f3462cba4c054939aac3dd Mon Sep 17 00:00:00 2001 From: ThibG Date: Sat, 29 Jun 2019 18:32:06 +0200 Subject: [Glitch] Use ScrollToOptions for smooth scrolling if supported Port 84ff3938426da348e31651dfad376d83a9784343 to glitch-soc Signed-off-by: Thibaut Girka --- app/javascript/flavours/glitch/util/scroll.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'app/javascript/flavours/glitch/util') diff --git a/app/javascript/flavours/glitch/util/scroll.js b/app/javascript/flavours/glitch/util/scroll.js index 2af07e0fb..84fe58269 100644 --- a/app/javascript/flavours/glitch/util/scroll.js +++ b/app/javascript/flavours/glitch/util/scroll.js @@ -26,5 +26,7 @@ const scroll = (node, key, target) => { }; }; -export const scrollRight = (node, position) => scroll(node, 'scrollLeft', position); -export const scrollTop = (node) => scroll(node, 'scrollTop', 0); +const isScrollBehaviorSupported = 'scrollBehavior' in document.documentElement.style; + +export const scrollRight = (node, position) => isScrollBehaviorSupported ? node.scrollTo({ left: position, behavior: 'smooth' }) : scroll(node, 'scrollLeft', position); +export const scrollTop = (node) => isScrollBehaviorSupported ? node.scrollTo({ top: 0, behavior: 'smooth' }) : scroll(node, 'scrollTop', 0); -- cgit From 1b074d2a50a331cdd03296170f04a75eec97a519 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sun, 14 Jul 2019 23:32:42 +0200 Subject: Add link to edit each listed filter --- .../flavours/glitch/containers/status_container.js | 20 +++++++++++++++++++- .../flavours/glitch/styles/components/modal.scss | 9 +++++++++ app/javascript/flavours/glitch/util/backend_links.js | 1 + 3 files changed, 29 insertions(+), 1 deletion(-) (limited to 'app/javascript/flavours/glitch/util') diff --git a/app/javascript/flavours/glitch/containers/status_container.js b/app/javascript/flavours/glitch/containers/status_container.js index b85a7b869..bded66d09 100644 --- a/app/javascript/flavours/glitch/containers/status_container.js +++ b/app/javascript/flavours/glitch/containers/status_container.js @@ -26,9 +26,11 @@ import { openModal } from 'flavours/glitch/actions/modal'; import { changeLocalSetting } from 'flavours/glitch/actions/local_settings'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { boostModal, favouriteModal, deleteModal } from 'flavours/glitch/util/initial_state'; +import { filterEditLink } from 'flavours/glitch/util/backend_links'; import { showAlertForError } from '../actions/alerts'; import AccountContainer from 'flavours/glitch/containers/account_container'; import Spoilers from '../components/spoilers'; +import Icon from 'flavours/glitch/components/icon'; const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' }, @@ -42,6 +44,7 @@ const messages = defineMessages({ 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}}' }, + editFilter: { id: 'confirmations.unfilter.edit_filter', defaultMessage: 'Edit filter' }, }); const makeMapStateToProps = () => { @@ -211,7 +214,22 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
    - {matchingFilters.map(filter =>
  • {filter.get('phrase')}
  • )} + {matchingFilters.map(filter => ( +
  • + {filter.get('phrase')} + {!!filterEditLink && ' '} + {!!filterEditLink && ( + + + + )} +
  • + ))}
diff --git a/app/javascript/flavours/glitch/styles/components/modal.scss b/app/javascript/flavours/glitch/styles/components/modal.scss index b21968840..a98efee9f 100644 --- a/app/javascript/flavours/glitch/styles/components/modal.scss +++ b/app/javascript/flavours/glitch/styles/components/modal.scss @@ -849,4 +849,13 @@ margin-left: 12px; list-style: disc inside; } + + .filtered-status-edit-link { + color: $action-button-color; + text-decoration: none; + + &:hover { + text-decoration: underline + } + } } diff --git a/app/javascript/flavours/glitch/util/backend_links.js b/app/javascript/flavours/glitch/util/backend_links.js index 4fc03f919..bc82197be 100644 --- a/app/javascript/flavours/glitch/util/backend_links.js +++ b/app/javascript/flavours/glitch/util/backend_links.js @@ -4,3 +4,4 @@ export const signOutLink = '/auth/sign_out'; export const termsLink = '/terms'; export const accountAdminLink = (id) => `/admin/accounts/${id}`; export const statusAdminLink = (account_id, status_id) => `/admin/accounts/${account_id}/statuses/${status_id}`; +export const filterEditLink = (id) => `/filters/${id}/edit`; -- cgit From e91bf82083ac390a0cf229d8e94fa412fdec57ff Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 16 Jul 2019 06:30:47 +0200 Subject: [Glitch] Add option to disable real-time updates in web UI Port 729723f857d11434c0f78d63fe16537d77f1c77c to glitch-soc Signed-off-by: Thibaut Girka --- .../flavours/glitch/actions/notifications.js | 32 ++++++++++++----- .../flavours/glitch/actions/timelines.js | 41 +++++++++++++++------- .../flavours/glitch/components/load_pending.js | 22 ++++++++++++ .../flavours/glitch/components/scrollable_list.js | 13 ++++++- .../components/column_settings.js | 2 +- .../notifications/components/setting_toggle.js | 5 +-- .../glitch/features/notifications/index.js | 11 +++++- .../ui/containers/status_list_container.js | 5 ++- .../flavours/glitch/reducers/notifications.js | 28 ++++++++++----- .../flavours/glitch/reducers/timelines.js | 40 ++++++++++++++------- app/javascript/flavours/glitch/util/compare_id.js | 5 +-- .../flavours/glitch/util/initial_state.js | 1 + 12 files changed, 154 insertions(+), 51 deletions(-) create mode 100644 app/javascript/flavours/glitch/components/load_pending.js (limited to 'app/javascript/flavours/glitch/util') diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index c057a5298..0c2331374 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -12,6 +12,8 @@ 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 compareId from 'flavours/glitch/util/compare_id'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; @@ -32,8 +34,9 @@ export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL'; export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET'; -export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR'; -export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; +export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR'; +export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; +export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING'; export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT'; export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT'; @@ -52,6 +55,10 @@ const fetchRelatedRelationships = (dispatch, notifications) => { } }; +export const loadPending = () => ({ + type: NOTIFICATIONS_LOAD_PENDING, +}); + export function updateNotifications(notification, intlMessages, intlLocale) { return (dispatch, getState) => { const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true); @@ -83,6 +90,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) { dispatch({ type: NOTIFICATIONS_UPDATE, notification, + usePendingItems: preferPendingItems, meta: (playSound && !filtered) ? { sound: 'boop' } : undefined, }); @@ -136,10 +144,19 @@ export function expandNotifications({ maxId } = {}, done = noOp) { : excludeTypesFromFilter(activeFilter), }; - if (!maxId && notifications.get('items').size > 0) { - params.since_id = notifications.getIn(['items', 0, 'id']); + if (!params.max_id && (notifications.get('items', ImmutableList()).size + notifications.get('pendingItems', ImmutableList()).size) > 0) { + const a = notifications.getIn(['pendingItems', 0, 'id']); + const b = notifications.getIn(['items', 0, 'id']); + + if (a && b && compareId(a, b) > 0) { + params.since_id = a; + } else { + params.since_id = b || a; + } } + const isLoadingRecent = !!params.since_id; + dispatch(expandNotificationsRequest(isLoadingMore)); api(getState).get('/api/v1/notifications', { params }).then(response => { @@ -148,7 +165,7 @@ export function expandNotifications({ maxId } = {}, done = noOp) { dispatch(importFetchedAccounts(response.data.map(item => item.account))); dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status))); - dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore)); + dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore, isLoadingRecent && preferPendingItems)); fetchRelatedRelationships(dispatch, response.data); done(); }).catch(error => { @@ -165,13 +182,12 @@ export function expandNotificationsRequest(isLoadingMore) { }; }; -export function expandNotificationsSuccess(notifications, next, isLoadingMore) { +export function expandNotificationsSuccess(notifications, next, isLoadingMore, usePendingItems) { return { type: NOTIFICATIONS_EXPAND_SUCCESS, notifications, - accounts: notifications.map(item => item.account), - statuses: notifications.map(item => item.status).filter(status => !!status), next, + usePendingItems, skipLoading: !isLoadingMore, }; }; diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index cca571583..f5bc0fd23 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -1,6 +1,8 @@ import { importFetchedStatus, importFetchedStatuses } from './importer'; import api, { getLinks } from 'flavours/glitch/util/api'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import compareId from 'flavours/glitch/util/compare_id'; +import { usePendingItems as preferPendingItems } from 'flavours/glitch/util/initial_state'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; export const TIMELINE_DELETE = 'TIMELINE_DELETE'; @@ -10,10 +12,15 @@ export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST'; export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS'; export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; -export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; +export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; +export const TIMELINE_LOAD_PENDING = 'TIMELINE_LOAD_PENDING'; +export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; +export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; -export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; -export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; +export const loadPending = timeline => ({ + type: TIMELINE_LOAD_PENDING, + timeline, +}); export function updateTimeline(timeline, status, accept) { return dispatch => { @@ -27,6 +34,7 @@ export function updateTimeline(timeline, status, accept) { type: TIMELINE_UPDATE, timeline, status, + usePendingItems: preferPendingItems, }); }; }; @@ -71,8 +79,15 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { return; } - if (!params.max_id && !params.pinned && timeline.get('items', ImmutableList()).size > 0) { - params.since_id = timeline.getIn(['items', 0]); + if (!params.max_id && !params.pinned && (timeline.get('items', ImmutableList()).size + timeline.get('pendingItems', ImmutableList()).size) > 0) { + const a = timeline.getIn(['pendingItems', 0]); + const b = timeline.getIn(['items', 0]); + + if (a && b && compareId(a, b) > 0) { + params.since_id = a; + } else { + params.since_id = b || a; + } } const isLoadingRecent = !!params.since_id; @@ -82,7 +97,7 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { api(getState).get(path, { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(importFetchedStatuses(response.data)); - dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore)); + dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems)); done(); }).catch(error => { dispatch(expandTimelineFail(timelineId, error, isLoadingMore)); @@ -117,7 +132,7 @@ export function expandTimelineRequest(timeline, isLoadingMore) { }; }; -export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore) { +export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore, usePendingItems) { return { type: TIMELINE_EXPAND_SUCCESS, timeline, @@ -125,6 +140,7 @@ export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadi next, partial, isLoadingRecent, + usePendingItems, skipLoading: !isLoadingMore, }; }; @@ -153,9 +169,8 @@ export function connectTimeline(timeline) { }; }; -export function disconnectTimeline(timeline) { - return { - type: TIMELINE_DISCONNECT, - timeline, - }; -}; +export const disconnectTimeline = timeline => ({ + type: TIMELINE_DISCONNECT, + timeline, + usePendingItems: preferPendingItems, +}); diff --git a/app/javascript/flavours/glitch/components/load_pending.js b/app/javascript/flavours/glitch/components/load_pending.js new file mode 100644 index 000000000..7e2702403 --- /dev/null +++ b/app/javascript/flavours/glitch/components/load_pending.js @@ -0,0 +1,22 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import PropTypes from 'prop-types'; + +export default class LoadPending extends React.PureComponent { + + static propTypes = { + onClick: PropTypes.func, + count: PropTypes.number, + } + + render() { + const { count } = this.props; + + return ( + + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/scrollable_list.js b/app/javascript/flavours/glitch/components/scrollable_list.js index 462185bbc..5f42bdd8b 100644 --- a/app/javascript/flavours/glitch/components/scrollable_list.js +++ b/app/javascript/flavours/glitch/components/scrollable_list.js @@ -3,6 +3,7 @@ import { ScrollContainer } from 'react-router-scroll-4'; import PropTypes from 'prop-types'; import IntersectionObserverArticleContainer from 'flavours/glitch/containers/intersection_observer_article_container'; import LoadMore from './load_more'; +import LoadPending from './load_pending'; import IntersectionObserverWrapper from 'flavours/glitch/util/intersection_observer_wrapper'; import { throttle } from 'lodash'; import { List as ImmutableList } from 'immutable'; @@ -21,6 +22,7 @@ export default class ScrollableList extends PureComponent { static propTypes = { scrollKey: PropTypes.string.isRequired, onLoadMore: PropTypes.func, + onLoadPending: PropTypes.func, onScrollToTop: PropTypes.func, onScroll: PropTypes.func, trackScroll: PropTypes.bool, @@ -28,6 +30,7 @@ export default class ScrollableList extends PureComponent { isLoading: PropTypes.bool, showLoading: PropTypes.bool, hasMore: PropTypes.bool, + numPending: PropTypes.number, prepend: PropTypes.node, alwaysPrepend: PropTypes.bool, emptyMessage: PropTypes.node, @@ -222,12 +225,18 @@ export default class ScrollableList extends PureComponent { return !(location.state && location.state.mastodonModalOpen); } + handleLoadPending = e => { + e.preventDefault(); + this.props.onLoadPending(); + } + render () { - const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props; + const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props; const { fullscreen } = this.state; const childrenCount = React.Children.count(children); const loadMore = (hasMore && onLoadMore) ? : null; + const loadPending = (numPending > 0) ? : null; let scrollableArea = null; if (showLoading) { @@ -248,6 +257,8 @@ export default class ScrollableList extends PureComponent {
{prepend} + {loadPending} + {React.Children.map(this.props.children, (child, index) => (
- } /> + } />
diff --git a/app/javascript/flavours/glitch/features/notifications/components/setting_toggle.js b/app/javascript/flavours/glitch/features/notifications/components/setting_toggle.js index ac2211e48..0264b6815 100644 --- a/app/javascript/flavours/glitch/features/notifications/components/setting_toggle.js +++ b/app/javascript/flavours/glitch/features/notifications/components/setting_toggle.js @@ -12,6 +12,7 @@ export default class SettingToggle extends React.PureComponent { label: PropTypes.node.isRequired, meta: PropTypes.node, onChange: PropTypes.func.isRequired, + defaultValue: PropTypes.bool, } onChange = ({ target }) => { @@ -19,12 +20,12 @@ export default class SettingToggle extends React.PureComponent { } render () { - const { prefix, settings, settingPath, label, meta } = this.props; + const { prefix, settings, settingPath, label, meta, defaultValue } = this.props; const id = ['setting-toggle', prefix, ...settingPath].filter(Boolean).join('-'); return (
- + {meta && {meta}}
diff --git a/app/javascript/flavours/glitch/features/notifications/index.js b/app/javascript/flavours/glitch/features/notifications/index.js index f2a1ccc3b..bf805c69a 100644 --- a/app/javascript/flavours/glitch/features/notifications/index.js +++ b/app/javascript/flavours/glitch/features/notifications/index.js @@ -10,6 +10,7 @@ import { scrollTopNotifications, mountNotifications, unmountNotifications, + loadPending, } from 'flavours/glitch/actions/notifications'; import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; import NotificationContainer from './containers/notification_container'; @@ -48,6 +49,7 @@ const mapStateToProps = state => ({ isLoading: state.getIn(['notifications', 'isLoading'], true), isUnread: state.getIn(['notifications', 'unread']) > 0, hasMore: state.getIn(['notifications', 'hasMore']), + numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size, notifCleaningActive: state.getIn(['notifications', 'cleaningMode']), }); @@ -80,6 +82,7 @@ export default class Notifications extends React.PureComponent { isUnread: PropTypes.bool, multiColumn: PropTypes.bool, hasMore: PropTypes.bool, + numPending: PropTypes.number, localSettings: ImmutablePropTypes.map, notifCleaningActive: PropTypes.bool, onEnterCleaningMode: PropTypes.func, @@ -100,6 +103,10 @@ export default class Notifications extends React.PureComponent { this.props.dispatch(expandNotifications({ maxId: last && last.get('id') })); }, 300, { leading: true }); + handleLoadPending = () => { + this.props.dispatch(loadPending()); + }; + handleScrollToTop = debounce(() => { this.props.dispatch(scrollTopNotifications(true)); }, 100); @@ -170,7 +177,7 @@ export default class Notifications extends React.PureComponent { } render () { - const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, showFilterBar } = this.props; + const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar } = this.props; const pinned = !!columnId; const emptyMessage = ; @@ -212,8 +219,10 @@ export default class Notifications extends React.PureComponent { isLoading={isLoading} showLoading={isLoading && notifications.size === 0} hasMore={hasMore} + numPending={numPending} emptyMessage={emptyMessage} onLoadMore={this.handleLoadOlder} + onLoadPending={this.handleLoadPending} onScrollToTop={this.handleScrollToTop} onScroll={this.handleScroll} shouldUpdateScroll={shouldUpdateScroll} diff --git a/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js b/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js index deb8b7763..4ca853563 100644 --- a/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js +++ b/app/javascript/flavours/glitch/features/ui/containers/status_list_container.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import StatusList from 'flavours/glitch/components/status_list'; -import { scrollTopTimeline } from 'flavours/glitch/actions/timelines'; +import { scrollTopTimeline, loadPending } from 'flavours/glitch/actions/timelines'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { createSelector } from 'reselect'; import { debounce } from 'lodash'; @@ -62,6 +62,7 @@ const makeMapStateToProps = () => { isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true), isPartial: state.getIn(['timelines', timelineId, 'isPartial'], false), hasMore: state.getIn(['timelines', timelineId, 'hasMore']), + numPending: state.getIn(['timelines', timelineId, 'pendingItems'], ImmutableList()).size, }); return mapStateToProps; @@ -77,6 +78,8 @@ const mapDispatchToProps = (dispatch, { timelineId }) => ({ dispatch(scrollTopTimeline(timelineId, false)); }, 100), + onLoadPending: () => dispatch(loadPending(timelineId)), + }); export default connect(makeMapStateToProps, mapDispatchToProps)(StatusList); diff --git a/app/javascript/flavours/glitch/reducers/notifications.js b/app/javascript/flavours/glitch/reducers/notifications.js index 5bbf9c822..d057f8f83 100644 --- a/app/javascript/flavours/glitch/reducers/notifications.js +++ b/app/javascript/flavours/glitch/reducers/notifications.js @@ -9,6 +9,7 @@ import { NOTIFICATIONS_FILTER_SET, NOTIFICATIONS_CLEAR, NOTIFICATIONS_SCROLL_TOP, + NOTIFICATIONS_LOAD_PENDING, NOTIFICATIONS_DELETE_MARKED_REQUEST, NOTIFICATIONS_DELETE_MARKED_SUCCESS, NOTIFICATION_MARK_FOR_DELETE, @@ -25,6 +26,7 @@ import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import compareId from 'flavours/glitch/util/compare_id'; const initialState = ImmutableMap({ + pendingItems: ImmutableList(), items: ImmutableList(), hasMore: true, top: false, @@ -46,7 +48,11 @@ const notificationToMap = (state, notification) => ImmutableMap({ status: notification.status ? notification.status.id : null, }); -const normalizeNotification = (state, notification) => { +const normalizeNotification = (state, notification, usePendingItems) => { + if (usePendingItems) { + return state.update('pendingItems', list => list.unshift(notificationToMap(state, notification))); + } + const top = !shouldCountUnreadNotifications(state); if (top) { @@ -64,7 +70,7 @@ const normalizeNotification = (state, notification) => { }); }; -const expandNormalizedNotifications = (state, notifications, next) => { +const expandNormalizedNotifications = (state, notifications, next, usePendingItems) => { const top = !(shouldCountUnreadNotifications(state)); const lastReadId = state.get('lastReadId'); let items = ImmutableList(); @@ -75,7 +81,7 @@ const expandNormalizedNotifications = (state, notifications, next) => { return state.withMutations(mutable => { if (!items.isEmpty()) { - mutable.update('items', list => { + mutable.update(usePendingItems ? 'pendingItems' : 'items', list => { const lastIndex = 1 + list.findLastIndex( item => item !== null && (compareId(item.get('id'), items.last().get('id')) > 0 || item.get('id') === items.last().get('id')) ); @@ -105,7 +111,8 @@ const expandNormalizedNotifications = (state, notifications, next) => { }; const filterNotifications = (state, relationship) => { - return state.update('items', list => list.filterNot(item => item !== null && item.get('account') === relationship.id)); + const helper = list => list.filterNot(item => item !== null && item.get('account') === relationship.id); + return state.update('items', helper).update('pendingItems', helper); }; const clearUnread = (state) => { @@ -131,7 +138,8 @@ const deleteByStatus = (state, statusId) => { const deletedUnread = state.get('items').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0); state = state.update('unread', unread => unread - deletedUnread.size); } - return state.update('items', list => list.filterNot(item => item !== null && item.get('status') === statusId)); + const helper = list => list.filterNot(item => item !== null && item.get('status') === statusId); + return state.update('items', helper).update('pendingItems', helper); }; const markForDelete = (state, notificationId, yes) => { @@ -192,6 +200,8 @@ export default function notifications(state = initialState, action) { return state.update('mounted', count => count - 1); case NOTIFICATIONS_SET_VISIBILITY: return updateVisibility(state, action.visibility); + case NOTIFICATIONS_LOAD_PENDING: + return state.update('items', list => state.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0); case NOTIFICATIONS_EXPAND_REQUEST: case NOTIFICATIONS_DELETE_MARKED_REQUEST: return state.set('isLoading', true); @@ -203,20 +213,20 @@ export default function notifications(state = initialState, action) { case NOTIFICATIONS_SCROLL_TOP: return updateTop(state, action.top); case NOTIFICATIONS_UPDATE: - return normalizeNotification(state, action.notification); + return normalizeNotification(state, action.notification, action.usePendingItems); case NOTIFICATIONS_EXPAND_SUCCESS: - return expandNormalizedNotifications(state, action.notifications, action.next); + return expandNormalizedNotifications(state, action.notifications, action.next, action.usePendingItems); case ACCOUNT_BLOCK_SUCCESS: return filterNotifications(state, action.relationship); case ACCOUNT_MUTE_SUCCESS: return action.relationship.muting_notifications ? filterNotifications(state, action.relationship) : state; case NOTIFICATIONS_CLEAR: - return state.set('items', ImmutableList()).set('hasMore', false); + return state.set('items', ImmutableList()).set('pendingItems', ImmutableList()).set('hasMore', false); case TIMELINE_DELETE: return deleteByStatus(state, action.id); case TIMELINE_DISCONNECT: return action.timeline === 'home' ? - state.update('items', items => items.first() ? items.unshift(null) : items) : + state.update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) : state; case NOTIFICATION_MARK_FOR_DELETE: diff --git a/app/javascript/flavours/glitch/reducers/timelines.js b/app/javascript/flavours/glitch/reducers/timelines.js index 440b370e6..9b016a4c6 100644 --- a/app/javascript/flavours/glitch/reducers/timelines.js +++ b/app/javascript/flavours/glitch/reducers/timelines.js @@ -8,6 +8,7 @@ import { TIMELINE_SCROLL_TOP, TIMELINE_CONNECT, TIMELINE_DISCONNECT, + TIMELINE_LOAD_PENDING, } from 'flavours/glitch/actions/timelines'; import { ACCOUNT_BLOCK_SUCCESS, @@ -25,10 +26,11 @@ const initialTimeline = ImmutableMap({ top: true, isLoading: false, hasMore: true, + pendingItems: ImmutableList(), items: ImmutableList(), }); -const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent) => { +const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent, usePendingItems) => { return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { mMap.set('isLoading', false); mMap.set('isPartial', isPartial); @@ -38,7 +40,7 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is if (timeline.endsWith(':pinned')) { mMap.set('items', statuses.map(status => status.get('id'))); } else if (!statuses.isEmpty()) { - mMap.update('items', ImmutableList(), oldIds => { + mMap.update(usePendingItems ? 'pendingItems' : 'items', ImmutableList(), oldIds => { const newIds = statuses.map(status => status.get('id')); const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1; const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0); @@ -56,7 +58,15 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is })); }; -const updateTimeline = (state, timeline, status) => { +const updateTimeline = (state, timeline, status, usePendingItems) => { + if (usePendingItems) { + if (state.getIn([timeline, 'pendingItems'], ImmutableList()).includes(status.get('id')) || state.getIn([timeline, 'items'], ImmutableList()).includes(status.get('id'))) { + return state; + } + + return state.update(timeline, initialTimeline, map => map.update('pendingItems', list => list.unshift(status.get('id')))); + } + const top = state.getIn([timeline, 'top']); const ids = state.getIn([timeline, 'items'], ImmutableList()); const includesId = ids.includes(status.get('id')); @@ -77,8 +87,10 @@ const updateTimeline = (state, timeline, status) => { const deleteStatus = (state, id, accountId, references, exclude_account = null) => { state.keySeq().forEach(timeline => { - if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`))) - state = state.updateIn([timeline, 'items'], list => list.filterNot(item => item === id)); + if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`))) { + const helper = list => list.filterNot(item => item === id); + state = state.updateIn([timeline, 'items'], helper).updateIn([timeline, 'pendingItems'], helper); + } }); // Remove reblogs of deleted status @@ -108,11 +120,10 @@ const filterTimelines = (state, relationship, statuses) => { return state; }; -const filterTimeline = (timeline, state, relationship, statuses) => - state.updateIn([timeline, 'items'], ImmutableList(), list => - list.filterNot(statusId => - statuses.getIn([statusId, 'account']) === relationship.id - )); +const filterTimeline = (timeline, state, relationship, statuses) => { + const helper = list => list.filterNot(statusId => statuses.getIn([statusId, 'account']) === relationship.id); + return state.updateIn([timeline, 'items'], ImmutableList(), helper).updateIn([timeline, 'pendingItems'], ImmutableList(), helper); +}; const updateTop = (state, timeline, top) => { return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { @@ -123,14 +134,17 @@ const updateTop = (state, timeline, top) => { export default function timelines(state = initialState, action) { switch(action.type) { + case TIMELINE_LOAD_PENDING: + return state.update(action.timeline, initialTimeline, map => + map.update('items', list => map.get('pendingItems').concat(list.take(40))).set('pendingItems', ImmutableList()).set('unread', 0)); case TIMELINE_EXPAND_REQUEST: return state.update(action.timeline, initialTimeline, map => map.set('isLoading', true)); case TIMELINE_EXPAND_FAIL: return state.update(action.timeline, initialTimeline, map => map.set('isLoading', false)); case TIMELINE_EXPAND_SUCCESS: - return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent); + return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent, action.usePendingItems); case TIMELINE_UPDATE: - return updateTimeline(state, action.timeline, fromJS(action.status)); + return updateTimeline(state, action.timeline, fromJS(action.status), action.usePendingItems); case TIMELINE_DELETE: return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf); case TIMELINE_CLEAR: @@ -148,7 +162,7 @@ export default function timelines(state = initialState, action) { return state.update( action.timeline, initialTimeline, - map => map.set('online', false).update('items', items => items.first() ? items.unshift(null) : items) + map => map.set('online', false).update(action.usePendingItems ? 'pendingItems' : 'items', items => items.first() ? items.unshift(null) : items) ); default: return state; diff --git a/app/javascript/flavours/glitch/util/compare_id.js b/app/javascript/flavours/glitch/util/compare_id.js index aaff66481..66cf51c4b 100644 --- a/app/javascript/flavours/glitch/util/compare_id.js +++ b/app/javascript/flavours/glitch/util/compare_id.js @@ -1,10 +1,11 @@ -export default function compareId(id1, id2) { +export default function compareId (id1, id2) { if (id1 === id2) { return 0; } + if (id1.length === id2.length) { return id1 > id2 ? 1 : -1; } else { return id1.length > id2.length ? 1 : -1; } -} +}; diff --git a/app/javascript/flavours/glitch/util/initial_state.js b/app/javascript/flavours/glitch/util/initial_state.js index e8811a6ce..caaa79bb3 100644 --- a/app/javascript/flavours/glitch/util/initial_state.js +++ b/app/javascript/flavours/glitch/util/initial_state.js @@ -30,5 +30,6 @@ export const isStaff = getMeta('is_staff'); export const defaultContentType = getMeta('default_content_type'); export const forceSingleColumn = getMeta('advanced_layout') === false; export const useBlurhash = getMeta('use_blurhash'); +export const usePendingItems = getMeta('use_pending_items'); export default initialState; -- cgit From c1231a846ac7b4a83b5b5b05384bca670d66ccde Mon Sep 17 00:00:00 2001 From: ThibG Date: Sun, 21 Jul 2019 18:10:40 +0200 Subject: [Glitch] Play animated custom emoji on hover Port 7de8c51873b51d8450f7a6597a43d454964d0407 to glitch-soc --- .../flavours/glitch/components/display_name.js | 44 +++++++++++++++++++++- .../flavours/glitch/components/status_content.js | 32 ++++++++++++++++ .../glitch/features/account/components/header.js | 43 ++++++++++++++++++++- app/javascript/flavours/glitch/packs/public.js | 10 +++++ app/javascript/flavours/glitch/util/emoji/index.js | 2 +- 5 files changed, 128 insertions(+), 3 deletions(-) (limited to 'app/javascript/flavours/glitch/util') diff --git a/app/javascript/flavours/glitch/components/display_name.js b/app/javascript/flavours/glitch/components/display_name.js index 7626fb25c..9d8c4a775 100644 --- a/app/javascript/flavours/glitch/components/display_name.js +++ b/app/javascript/flavours/glitch/components/display_name.js @@ -2,6 +2,7 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import classNames from 'classnames'; +import { autoPlayGif } from 'flavours/glitch/util/initial_state'; export default class DisplayName extends React.PureComponent { @@ -14,6 +15,47 @@ export default class DisplayName extends React.PureComponent { handleClick: PropTypes.func, }; + _updateEmojis () { + const node = this.node; + + if (!node || autoPlayGif) { + return; + } + + const emojis = node.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + if (emoji.classList.contains('status-emoji')) { + continue; + } + emoji.classList.add('status-emoji'); + + emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); + emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + } + } + + componentDidMount () { + this._updateEmojis(); + } + + componentDidUpdate () { + this._updateEmojis(); + } + + handleEmojiMouseEnter = ({ target }) => { + target.src = target.getAttribute('data-original'); + } + + handleEmojiMouseLeave = ({ target }) => { + target.src = target.getAttribute('data-static'); + } + + setRef = (c) => { + this.node = c; + } + render() { const { account, className, inline, localDomain, others, onAccountClick } = this.props; @@ -58,7 +100,7 @@ export default class DisplayName extends React.PureComponent { } return ( - + {displayName} {inline ? ' ' : null} {suffix} diff --git a/app/javascript/flavours/glitch/components/status_content.js b/app/javascript/flavours/glitch/components/status_content.js index 07a0d1d5d..6e357fba0 100644 --- a/app/javascript/flavours/glitch/components/status_content.js +++ b/app/javascript/flavours/glitch/components/status_content.js @@ -5,6 +5,7 @@ import { isRtl } from 'flavours/glitch/util/rtl'; import { FormattedMessage } from 'react-intl'; import Permalink from './permalink'; import classnames from 'classnames'; +import { autoPlayGif } from 'flavours/glitch/util/initial_state'; export default class StatusContent extends React.PureComponent { @@ -57,12 +58,35 @@ export default class StatusContent extends React.PureComponent { } } + _updateStatusEmojis () { + const node = this.node; + + if (!node || autoPlayGif) { + return; + } + + const emojis = node.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + if (emoji.classList.contains('status-emoji')) { + continue; + } + emoji.classList.add('status-emoji'); + + emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); + emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + } + } + componentDidMount () { this._updateStatusLinks(); + this._updateStatusEmojis(); } componentDidUpdate () { this._updateStatusLinks(); + this._updateStatusEmojis(); if (this.props.onUpdate) this.props.onUpdate(); } @@ -86,6 +110,14 @@ export default class StatusContent extends React.PureComponent { } } + handleEmojiMouseEnter = ({ target }) => { + target.src = target.getAttribute('data-original'); + } + + handleEmojiMouseLeave = ({ target }) => { + target.src = target.getAttribute('data-static'); + } + handleMouseDown = (e) => { this.startXY = [e.clientX, e.clientY]; } diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js index 43c4f0d32..e9437c0a9 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.js +++ b/app/javascript/flavours/glitch/features/account/components/header.js @@ -71,6 +71,47 @@ class Header extends ImmutablePureComponent { window.open('/settings/profile', '_blank'); } + _updateEmojis () { + const node = this.node; + + if (!node || autoPlayGif) { + return; + } + + const emojis = node.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + if (emoji.classList.contains('status-emoji')) { + continue; + } + emoji.classList.add('status-emoji'); + + emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false); + emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false); + } + } + + componentDidMount () { + this._updateEmojis(); + } + + componentDidUpdate () { + this._updateEmojis(); + } + + handleEmojiMouseEnter = ({ target }) => { + target.src = target.getAttribute('data-original'); + } + + handleEmojiMouseLeave = ({ target }) => { + target.src = target.getAttribute('data-static'); + } + + setRef = (c) => { + this.node = c; + } + render () { const { account, intl, domain, identity_proofs } = this.props; @@ -193,7 +234,7 @@ class Header extends ImmutablePureComponent { const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct'); return ( -
+
{info} diff --git a/app/javascript/flavours/glitch/packs/public.js b/app/javascript/flavours/glitch/packs/public.js index da0b4c8e0..9f88b0e04 100644 --- a/app/javascript/flavours/glitch/packs/public.js +++ b/app/javascript/flavours/glitch/packs/public.js @@ -4,6 +4,7 @@ import ready from 'flavours/glitch/util/ready'; function main() { const IntlMessageFormat = require('intl-messageformat').default; const { timeAgoString } = require('flavours/glitch/components/relative_timestamp'); + const { delegate } = require('rails-ujs'); const emojify = require('flavours/glitch/util/emoji').default; const { getLocale } = require('locales'); const { messages } = getLocale(); @@ -23,6 +24,12 @@ function main() { } }; + const getEmojiAnimationHandler = (swapTo) => { + return ({ target }) => { + target.src = target.getAttribute(swapTo); + }; + }; + ready(() => { const locale = document.documentElement.lang; @@ -94,6 +101,9 @@ function main() { document.head.appendChild(scrollbarWidthStyle); scrollbarWidthStyle.sheet.insertRule(`body.with-modals--active { margin-right: ${scrollbarWidth}px; }`, 0); } + + delegate(document, '.custom-emoji', 'mouseover', getEmojiAnimationHandler('data-original')); + delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static')); }); } diff --git a/app/javascript/flavours/glitch/util/emoji/index.js b/app/javascript/flavours/glitch/util/emoji/index.js index e6fcaf0dc..2f40f9b08 100644 --- a/app/javascript/flavours/glitch/util/emoji/index.js +++ b/app/javascript/flavours/glitch/util/emoji/index.js @@ -29,7 +29,7 @@ const emojify = (str, customEmojis = {}) => { // if you want additional emoji handler, add statements below which set replacement and return true. if (shortname in customEmojis) { const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url; - replacement = `${shortname}`; + replacement = `${shortname}`; return true; } return false; -- cgit From 49a28e69a520d5994352e231e9879a5093af5916 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Thu, 1 Aug 2019 14:35:17 +0200 Subject: Move decodeIDNA to app/javascript/flavours/glitch/util --- .../flavours/glitch/features/status/components/card.js | 10 +--------- app/javascript/flavours/glitch/util/idna.js | 10 ++++++++++ 2 files changed, 11 insertions(+), 9 deletions(-) create mode 100644 app/javascript/flavours/glitch/util/idna.js (limited to 'app/javascript/flavours/glitch/util') diff --git a/app/javascript/flavours/glitch/features/status/components/card.js b/app/javascript/flavours/glitch/features/status/components/card.js index f974a87a1..108b6e3b2 100644 --- a/app/javascript/flavours/glitch/features/status/components/card.js +++ b/app/javascript/flavours/glitch/features/status/components/card.js @@ -4,15 +4,7 @@ import Immutable from 'immutable'; import ImmutablePropTypes from 'react-immutable-proptypes'; import punycode from 'punycode'; import classnames from 'classnames'; - -const IDNA_PREFIX = 'xn--'; - -const decodeIDNA = domain => { - return domain - .split('.') - .map(part => part.indexOf(IDNA_PREFIX) === 0 ? punycode.decode(part.slice(IDNA_PREFIX.length)) : part) - .join('.'); -}; +import { decode as decodeIDNA } from 'flavours/glitch/util/idna'; const getHostname = url => { const parser = document.createElement('a'); diff --git a/app/javascript/flavours/glitch/util/idna.js b/app/javascript/flavours/glitch/util/idna.js new file mode 100644 index 000000000..efab5bacf --- /dev/null +++ b/app/javascript/flavours/glitch/util/idna.js @@ -0,0 +1,10 @@ +import punycode from 'punycode'; + +const IDNA_PREFIX = 'xn--'; + +export const decode = domain => { + return domain + .split('.') + .map(part => part.indexOf(IDNA_PREFIX) === 0 ? punycode.decode(part.slice(IDNA_PREFIX.length)) : part) + .join('.'); +}; -- cgit From 381dbb6569b07554a2543082cbb2e736fb425e2a Mon Sep 17 00:00:00 2001 From: ThibG Date: Tue, 6 Aug 2019 12:08:19 +0200 Subject: [Glitch] Fix image uploads being perfectly white when canvas read access is blocked Port 111a0628fc161df4d76967d7dc7116b8a43fe8e2 to glitch-soc Signed-off-by: Thibaut Girka --- app/javascript/flavours/glitch/util/resize_image.js | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'app/javascript/flavours/glitch/util') diff --git a/app/javascript/flavours/glitch/util/resize_image.js b/app/javascript/flavours/glitch/util/resize_image.js index bbdbc865e..a8ec5f3fa 100644 --- a/app/javascript/flavours/glitch/util/resize_image.js +++ b/app/javascript/flavours/glitch/util/resize_image.js @@ -67,6 +67,14 @@ const processImage = (img, { width, height, orientation, type = 'image/png' }) = context.drawImage(img, 0, 0, width, height); + // The Tor Browser and maybe other browsers may prevent reading from canvas + // and return an all-white image instead. Assume reading failed if the resized + // image is perfectly white. + const imageData = context.getImageData(0, 0, width, height); + if (imageData.every(value => value === 255)) { + throw 'Failed to read from canvas'; + } + canvas.toBlob(resolve, type); }); -- cgit From 597ea5687a3601c6cc462831bf570bae05b08240 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Mon, 12 Aug 2019 15:31:20 +0200 Subject: Add option to disable emoji replacements Fixes #647 The option is found in `/settings` (because that was easier to write it this way) but only affects the glitch-soc front-end. --- app/controllers/settings/preferences_controller.rb | 1 + app/javascript/flavours/glitch/util/emoji/index.js | 6 +++--- app/javascript/flavours/glitch/util/initial_state.js | 1 + app/lib/user_settings_decorator.rb | 5 +++++ app/models/user.rb | 2 +- app/serializers/initial_state_serializer.rb | 1 + app/views/settings/preferences/appearance/show.html.haml | 1 + config/locales/simple_form.en.yml | 1 + config/settings.yml | 1 + 9 files changed, 15 insertions(+), 4 deletions(-) (limited to 'app/javascript/flavours/glitch/util') diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 418ea5d7a..f05dbe0ea 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -49,6 +49,7 @@ class Settings::PreferencesController < Settings::BaseController :setting_expand_spoilers, :setting_reduce_motion, :setting_system_font_ui, + :setting_system_emoji_font, :setting_noindex, :setting_hide_network, :setting_hide_followers_count, diff --git a/app/javascript/flavours/glitch/util/emoji/index.js b/app/javascript/flavours/glitch/util/emoji/index.js index 2f40f9b08..b2d13cc95 100644 --- a/app/javascript/flavours/glitch/util/emoji/index.js +++ b/app/javascript/flavours/glitch/util/emoji/index.js @@ -1,4 +1,4 @@ -import { autoPlayGif } from 'flavours/glitch/util/initial_state'; +import { autoPlayGif, useSystemEmojiFont } from 'flavours/glitch/util/initial_state'; import unicodeMapping from './emoji_unicode_mapping_light'; import Trie from 'substring-trie'; @@ -12,7 +12,7 @@ const emojify = (str, customEmojis = {}) => { let rtn = '', tagChars = tagCharsWithEmojis, invisible = 0; for (;;) { let match, i = 0, tag; - while (i < str.length && (tag = tagChars.indexOf(str[i])) === -1 && (invisible || !(match = trie.search(str.slice(i))))) { + while (i < str.length && (tag = tagChars.indexOf(str[i])) === -1 && (invisible || useSystemEmojiFont || !(match = trie.search(str.slice(i))))) { i += str.codePointAt(i) < 65536 ? 1 : 2; } let rend, replacement = ''; @@ -57,7 +57,7 @@ const emojify = (str, customEmojis = {}) => { } } i = rend; - } else { // matched to unicode emoji + } else if (!useSystemEmojiFont) { // matched to unicode emoji const { filename, shortCode } = unicodeMapping[match]; const title = shortCode ? `:${shortCode}:` : ''; replacement = `${match}`; diff --git a/app/javascript/flavours/glitch/util/initial_state.js b/app/javascript/flavours/glitch/util/initial_state.js index caaa79bb3..4b6227cac 100644 --- a/app/javascript/flavours/glitch/util/initial_state.js +++ b/app/javascript/flavours/glitch/util/initial_state.js @@ -31,5 +31,6 @@ export const defaultContentType = getMeta('default_content_type'); export const forceSingleColumn = getMeta('advanced_layout') === false; export const useBlurhash = getMeta('use_blurhash'); export const usePendingItems = getMeta('use_pending_items'); +export const useSystemEmojiFont = getMeta('system_emoji_font'); export default initialState; diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index a52172707..c822d54de 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -29,6 +29,7 @@ class UserSettingsDecorator user.settings['expand_spoilers'] = expand_spoilers_preference if change?('setting_expand_spoilers') user.settings['reduce_motion'] = reduce_motion_preference if change?('setting_reduce_motion') user.settings['system_font_ui'] = system_font_ui_preference if change?('setting_system_font_ui') + user.settings['system_emoji_font'] = system_emoji_font_preference if change?('setting_system_emoji_font') user.settings['noindex'] = noindex_preference if change?('setting_noindex') user.settings['hide_followers_count']= hide_followers_count_preference if change?('setting_hide_followers_count') user.settings['flavour'] = flavour_preference if change?('setting_flavour') @@ -79,6 +80,10 @@ class UserSettingsDecorator boolean_cast_setting 'setting_system_font_ui' end + def system_emoji_font_preference + boolean_cast_setting 'setting_system_emoji_font' + end + def auto_play_gif_preference boolean_cast_setting 'setting_auto_play_gif' end diff --git a/app/models/user.rb b/app/models/user.rb index 45a4b8989..341227d3e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -108,7 +108,7 @@ class User < ApplicationRecord :reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_media, :hide_network, :hide_followers_count, :expand_spoilers, :default_language, :aggregate_reblogs, :show_application, :advanced_layout, :use_blurhash, :use_pending_items, :trends, - :default_content_type, + :default_content_type, :system_emoji_font, to: :settings, prefix: :setting, allow_nil: false attr_reader :invite_code diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index c8da6e725..c8f6bec7a 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -53,6 +53,7 @@ class InitialStateSerializer < ActiveModel::Serializer store[:is_staff] = object.current_account.user.staff? store[:trends] = Setting.trends && object.current_account.user.setting_trends store[:default_content_type] = object.current_account.user.setting_default_content_type + store[:system_emoji_font] = object.current_account.user.setting_system_emoji_font end store diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml index 0bda49f44..900a7c6fb 100644 --- a/app/views/settings/preferences/appearance/show.html.haml +++ b/app/views/settings/preferences/appearance/show.html.haml @@ -21,6 +21,7 @@ = f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label, recommended: true = f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label = f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label + = f.input :setting_system_emoji_font, as: :boolean, wrapper: :with_label %h4= t 'appearance.discovery' diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 5da0cc45d..8f63df013 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -124,6 +124,7 @@ en: setting_reduce_motion: Reduce motion in animations setting_show_application: Disclose application used to send toots setting_skin: Skin + setting_system_emoji_font: Use system's default font for emojis (applies to Glitch flavour only) setting_system_font_ui: Use system's default font setting_theme: Site theme setting_trends: Show today's trends diff --git a/config/settings.yml b/config/settings.yml index 10836db3f..70a9a3a44 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -29,6 +29,7 @@ defaults: &defaults reduce_motion: false show_application: false system_font_ui: false + system_emoji_font: false noindex: false hide_followers_count: false enable_keybase: true -- cgit From 26b249ad2dd04372aa56103ac59f9ece2eb58f0c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 13 Aug 2019 12:22:33 +0200 Subject: [Glitch] Fix short number formatting for numbers above million in web UI Port 0e9668051e58b2ff4e48b82fa7cc17d385c7a5c9 to glitch-soc Signed-off-by: Thibaut Girka --- app/javascript/flavours/glitch/util/numbers.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'app/javascript/flavours/glitch/util') diff --git a/app/javascript/flavours/glitch/util/numbers.js b/app/javascript/flavours/glitch/util/numbers.js index fdd8269ae..f7e4ceb93 100644 --- a/app/javascript/flavours/glitch/util/numbers.js +++ b/app/javascript/flavours/glitch/util/numbers.js @@ -4,7 +4,9 @@ import { FormattedNumber } from 'react-intl'; export const shortNumberFormat = number => { if (number < 1000) { return ; - } else { + } else if (number < 1000000) { return K; + } else { + return M; } }; -- cgit From 4e9794f53391b3e8291b800af613bcf6d74550a9 Mon Sep 17 00:00:00 2001 From: ThibG Date: Thu, 15 Aug 2019 11:52:26 +0200 Subject: [Glitch] Fix client-side resizing of image uploads Port f178a01c11a4af077926dd035a0c4c44f6b3985c to glitch-soc Signed-off-by: Thibaut Girka --- app/javascript/flavours/glitch/util/resize_image.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/javascript/flavours/glitch/util') diff --git a/app/javascript/flavours/glitch/util/resize_image.js b/app/javascript/flavours/glitch/util/resize_image.js index a8ec5f3fa..d566edb03 100644 --- a/app/javascript/flavours/glitch/util/resize_image.js +++ b/app/javascript/flavours/glitch/util/resize_image.js @@ -71,7 +71,7 @@ const processImage = (img, { width, height, orientation, type = 'image/png' }) = // and return an all-white image instead. Assume reading failed if the resized // image is perfectly white. const imageData = context.getImageData(0, 0, width, height); - if (imageData.every(value => value === 255)) { + if (imageData.data.every(value => value === 255)) { throw 'Failed to read from canvas'; } -- cgit From 5c35c9c40968011ce6b1837bf2c420322b21c83d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 15 Aug 2019 17:24:45 +0200 Subject: [Glitch] Fix tesseract.js being part of the common chunk Port dfe60598899738250fc707db91048b871ab09282 to glitch-soc Signed-off-by: Thibaut Girka --- .../features/ui/components/focal_point_modal.js | 26 ++++++++++++---------- .../flavours/glitch/util/async-components.js | 4 ++++ 2 files changed, 18 insertions(+), 12 deletions(-) (limited to 'app/javascript/flavours/glitch/util') diff --git a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js index e5a0d029a..6dc47dca3 100644 --- a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js @@ -10,11 +10,11 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import IconButton from 'flavours/glitch/components/icon_button'; import Button from 'flavours/glitch/components/button'; import Video from 'flavours/glitch/features/video'; -import { TesseractWorker } from 'tesseract.js'; import Textarea from 'react-textarea-autosize'; import UploadProgress from 'flavours/glitch/features/compose/components/upload_progress'; import CharacterCounter from 'flavours/glitch/features/compose/components/character_counter'; import { length } from 'stringz'; +import { Tesseract as fetchTesseract } from 'flavours/glitch/util/async-components'; const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, @@ -148,19 +148,21 @@ class FocalPointModal extends ImmutablePureComponent { handleTextDetection = () => { const { media } = this.props; - const worker = new TesseractWorker({ - workerPath: `${assetHost}/packs/ocr/worker.min.js`, - corePath: `${assetHost}/packs/ocr/tesseract-core.wasm.js`, - langPath: `${assetHost}/ocr/lang-data`, - }); - this.setState({ detecting: true }); - worker.recognize(media.get('url')) - .progress(({ progress }) => this.setState({ progress })) - .finally(() => worker.terminate()) - .then(({ text }) => this.setState({ description: removeExtraLineBreaks(text), dirty: true, detecting: false })) - .catch(() => this.setState({ detecting: false })); + fetchTesseract().then(({ TesseractWorker }) => { + const worker = new TesseractWorker({ + workerPath: `${assetHost}/packs/ocr/worker.min.js`, + corePath: `${assetHost}/packs/ocr/tesseract-core.wasm.js`, + langPath: `${assetHost}/ocr/lang-data`, + }); + + worker.recognize(media.get('url')) + .progress(({ progress }) => this.setState({ progress })) + .finally(() => worker.terminate()) + .then(({ text }) => this.setState({ description: removeExtraLineBreaks(text), dirty: true, detecting: false })) + .catch(() => this.setState({ detecting: false })); + }).catch(() => this.setState({ detecting: false })); } render () { diff --git a/app/javascript/flavours/glitch/util/async-components.js b/app/javascript/flavours/glitch/util/async-components.js index f2aeda834..8f2e4c6e4 100644 --- a/app/javascript/flavours/glitch/util/async-components.js +++ b/app/javascript/flavours/glitch/util/async-components.js @@ -153,3 +153,7 @@ export function ListAdder () { export function Search () { return import(/*webpackChunkName: "features/glitch/async/search" */'flavours/glitch/features/search'); } + +export function Tesseract () { + return import(/*webpackChunkName: "tesseract" */'tesseract.js'); +} -- cgit From f11e9d552478c45f8559391e21528c89931a9b64 Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 26 Aug 2019 18:24:10 +0200 Subject: [Glitch] Add confirmation modal when logging out from the web app Port fd3d45d38f289815bed0adc7eaa3b15894ddb8fb to glitch-soc Signed-off-by: Thibaut Girka --- .../glitch/features/compose/components/header.js | 12 ++- .../compose/containers/header_container.js | 16 +++- .../glitch/features/ui/components/link_footer.js | 93 +++++++++++++++------- app/javascript/flavours/glitch/util/log_out.js | 34 ++++++++ 4 files changed, 124 insertions(+), 31 deletions(-) create mode 100644 app/javascript/flavours/glitch/util/log_out.js (limited to 'app/javascript/flavours/glitch/util') diff --git a/app/javascript/flavours/glitch/features/compose/components/header.js b/app/javascript/flavours/glitch/features/compose/components/header.js index 2e29084f2..8b0f540ef 100644 --- a/app/javascript/flavours/glitch/features/compose/components/header.js +++ b/app/javascript/flavours/glitch/features/compose/components/header.js @@ -53,8 +53,18 @@ class Header extends ImmutablePureComponent { showNotificationsBadge: PropTypes.bool, intl: PropTypes.object, onSettingsClick: PropTypes.func, + onLogout: PropTypes.func.isRequired, }; + handleLogoutClick = e => { + e.preventDefault(); + e.stopPropagation(); + + this.props.onLogout(); + + return false; + } + render () { const { intl, columns, unreadNotifications, showNotificationsBadge, onSettingsClick } = this.props; @@ -114,7 +124,7 @@ class Header extends ImmutablePureComponent { > diff --git a/app/javascript/flavours/glitch/features/compose/containers/header_container.js b/app/javascript/flavours/glitch/features/compose/containers/header_container.js index ce1dea319..b4dcb4d56 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/header_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/header_container.js @@ -1,6 +1,13 @@ import { openModal } from 'flavours/glitch/actions/modal'; import { connect } from 'react-redux'; +import { defineMessages, injectIntl } from 'react-intl'; import Header from '../components/header'; +import { logOut } from 'flavours/glitch/util/log_out'; + +const messages = defineMessages({ + logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' }, + logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' }, +}); const mapStateToProps = state => { return { @@ -16,6 +23,13 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ e.stopPropagation(); dispatch(openModal('SETTINGS', {})); }, + onLogout () { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.logoutMessage), + confirm: intl.formatMessage(messages.logoutConfirm), + onConfirm: () => logOut(), + })); + }, }); -export default connect(mapStateToProps, mapDispatchToProps)(Header); +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Header)); 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 3e724fffb..1712da83e 100644 --- a/app/javascript/flavours/glitch/features/ui/components/link_footer.js +++ b/app/javascript/flavours/glitch/features/ui/components/link_footer.js @@ -1,36 +1,71 @@ +import { connect } from 'react-redux'; import React from 'react'; import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; +import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; import { Link } from 'react-router-dom'; import { invitesEnabled, version, repository, source_url } from 'flavours/glitch/util/initial_state'; import { signOutLink } from 'flavours/glitch/util/backend_links'; +import { logOut } from 'flavours/glitch/util/log_out'; +import { openModal } from 'flavours/glitch/actions/modal'; -const LinkFooter = () => ( -
-
    - {invitesEnabled &&
  • ·
  • } -
  • ·
  • -
  • ·
  • -
  • ·
  • -
  • ·
  • -
  • ·
  • -
  • ·
  • -
  • -
- -

- glitch-soc/mastodon (v{version}), - Mastodon: Mastodon }} - /> -

-
-); - -LinkFooter.propTypes = { -}; +const messages = defineMessages({ + logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' }, + logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' }, +}); + +const mapDispatchToProps = (dispatch, { intl }) => ({ + onLogout () { + dispatch(openModal('CONFIRM', { + message: intl.formatMessage(messages.logoutMessage), + confirm: intl.formatMessage(messages.logoutConfirm), + onConfirm: () => logOut(), + })); + }, +}); + +export default @injectIntl +@connect(null, mapDispatchToProps) +class LinkFooter extends React.PureComponent { + + static propTypes = { + onLogout: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + handleLogoutClick = e => { + e.preventDefault(); + e.stopPropagation(); -export default LinkFooter; + this.props.onLogout(); + + return false; + } + + render () { + return ( +
+
    + {invitesEnabled &&
  • ·
  • } +
  • ·
  • +
  • ·
  • +
  • ·
  • +
  • ·
  • +
  • ·
  • +
  • ·
  • +
  • +
+ +

+ glitch-soc/mastodon (v{version}), + Mastodon: Mastodon }} + /> +

+
+ ); + } + +}; diff --git a/app/javascript/flavours/glitch/util/log_out.js b/app/javascript/flavours/glitch/util/log_out.js new file mode 100644 index 000000000..8e1659293 --- /dev/null +++ b/app/javascript/flavours/glitch/util/log_out.js @@ -0,0 +1,34 @@ +import Rails from 'rails-ujs'; +import { signOutLink } from 'flavours/glitch/util/backend_links'; + +export const logOut = () => { + const form = document.createElement('form'); + + const methodInput = document.createElement('input'); + methodInput.setAttribute('name', '_method'); + methodInput.setAttribute('value', 'delete'); + methodInput.setAttribute('type', 'hidden'); + form.appendChild(methodInput); + + const csrfToken = Rails.csrfToken(); + const csrfParam = Rails.csrfParam(); + + if (csrfParam && csrfToken) { + const csrfInput = document.createElement('input'); + csrfInput.setAttribute('name', csrfParam); + csrfInput.setAttribute('value', csrfToken); + csrfInput.setAttribute('type', 'hidden'); + form.appendChild(csrfInput); + } + + const submitButton = document.createElement('input'); + submitButton.setAttribute('type', 'submit'); + form.appendChild(submitButton); + + form.method = 'post'; + form.action = signOutLink; + form.style.display = 'none'; + + document.body.appendChild(form); + submitButton.click(); +}; -- cgit From 3aeaf9b897a7fff8ebb08055b06717e78e5cf914 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 23 Aug 2019 22:38:02 +0200 Subject: [Glitch] Add audio player Port 4190e31626907059aebf32b1be66715dacb989a9 to glitch-soc Signed-off-by: Thibaut Girka --- .../flavours/glitch/components/status.js | 30 ++- .../flavours/glitch/containers/media_container.js | 3 +- .../flavours/glitch/features/audio/index.js | 219 +++++++++++++++++++++ .../features/status/components/detailed_status.js | 18 +- .../features/ui/components/focal_point_modal.js | 14 +- .../flavours/glitch/features/video/index.js | 2 +- .../flavours/glitch/styles/components/media.scss | 35 ++++ .../glitch/styles/components/single_column.scss | 6 +- .../flavours/glitch/styles/components/status.scss | 9 +- .../flavours/glitch/util/async-components.js | 4 + 10 files changed, 325 insertions(+), 15 deletions(-) create mode 100644 app/javascript/flavours/glitch/features/audio/index.js (limited to 'app/javascript/flavours/glitch/util') diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 5b69ac4da..47e110128 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -10,7 +10,7 @@ import AttachmentList from './attachment_list'; import Card from '../features/status/components/card'; import { injectIntl, FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { MediaGallery, Video } from 'flavours/glitch/util/async-components'; +import { MediaGallery, Video, Audio } from 'flavours/glitch/util/async-components'; import { HotKeys } from 'react-hotkeys'; import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container'; import classNames from 'classnames'; @@ -443,11 +443,15 @@ class Status extends ImmutablePureComponent { } renderLoadingMediaGallery () { - return
; + return
; } renderLoadingVideoPlayer () { - return
; + return
; + } + + renderLoadingAudioPlayer () { + return
; } render () { @@ -561,7 +565,23 @@ class Status extends ImmutablePureComponent { media={status.get('media_attachments')} /> ); - } else if (['video', 'audio'].includes(attachments.getIn([0, 'type']))) { + } else if (attachments.getIn([0, 'type']) === 'audio') { + const attachment = status.getIn(['media_attachments', 0]); + + media = ( + + {Component => ( + + )} + + ); + mediaIcon = 'music'; + } else if (attachments.getIn([0, 'type']) === 'video') { const attachment = status.getIn(['media_attachments', 0]); media = ( @@ -584,7 +604,7 @@ class Status extends ImmutablePureComponent { />)} ); - mediaIcon = attachment.get('type') === 'video' ? 'video-camera' : 'music'; + mediaIcon = 'video-camera'; } else { // Media type is 'image' or 'gifv' media = ( diff --git a/app/javascript/flavours/glitch/containers/media_container.js b/app/javascript/flavours/glitch/containers/media_container.js index 1b480658f..c1738db4d 100644 --- a/app/javascript/flavours/glitch/containers/media_container.js +++ b/app/javascript/flavours/glitch/containers/media_container.js @@ -7,6 +7,7 @@ import MediaGallery from 'flavours/glitch/components/media_gallery'; import Video from 'flavours/glitch/features/video'; import Card from 'flavours/glitch/features/status/components/card'; import Poll from 'flavours/glitch/components/poll'; +import Audio from 'flavours/glitch/features/audio'; import ModalRoot from 'flavours/glitch/components/modal_root'; import MediaModal from 'flavours/glitch/features/ui/components/media_modal'; import { List as ImmutableList, fromJS } from 'immutable'; @@ -14,7 +15,7 @@ import { List as ImmutableList, fromJS } from 'immutable'; const { localeData, messages } = getLocale(); addLocaleData(localeData); -const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll }; +const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Audio }; export default class MediaContainer extends PureComponent { diff --git a/app/javascript/flavours/glitch/features/audio/index.js b/app/javascript/flavours/glitch/features/audio/index.js new file mode 100644 index 000000000..1daf56bc1 --- /dev/null +++ b/app/javascript/flavours/glitch/features/audio/index.js @@ -0,0 +1,219 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import WaveSurfer from 'wavesurfer.js'; +import { defineMessages, injectIntl } from 'react-intl'; +import { formatTime } from 'flavours/glitch/features/video'; +import Icon from 'flavours/glitch/components/icon'; +import classNames from 'classnames'; +import { throttle } from 'lodash'; + +const messages = defineMessages({ + play: { id: 'video.play', defaultMessage: 'Play' }, + pause: { id: 'video.pause', defaultMessage: 'Pause' }, + mute: { id: 'video.mute', defaultMessage: 'Mute sound' }, + unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' }, +}); + +const arrayOf = (length, fill) => (new Array(length)).fill(fill); + +export default @injectIntl +class Audio extends React.PureComponent { + + static propTypes = { + src: PropTypes.string.isRequired, + alt: PropTypes.string, + duration: PropTypes.number, + height: PropTypes.number, + preload: PropTypes.bool, + editable: PropTypes.bool, + intl: PropTypes.object.isRequired, + }; + + state = { + currentTime: 0, + duration: null, + paused: true, + muted: false, + volume: 0.5, + }; + + // hard coded in components.scss + // any way to get ::before values programatically? + + volWidth = 50; + + volOffset = 70; + + volHandleOffset = v => { + const offset = v * this.volWidth + this.volOffset; + return (offset > 110) ? 110 : offset; + } + + setVolumeRef = c => { + this.volume = c; + } + + setWaveformRef = c => { + this.waveform = c; + } + + componentDidMount () { + if (this.waveform) { + this._updateWaveform(); + } + } + + componentDidUpdate (prevProps) { + if (this.waveform && prevProps.src !== this.props.src) { + this._updateWaveform(); + } + } + + componentWillUnmount () { + if (this.wavesurfer) { + this.wavesurfer.destroy(); + this.wavesurfer = null; + } + } + + _updateWaveform () { + const { src, height, duration, preload } = this.props; + + const progressColor = window.getComputedStyle(document.querySelector('.audio-player__progress-placeholder')).getPropertyValue('background-color'); + const waveColor = window.getComputedStyle(document.querySelector('.audio-player__wave-placeholder')).getPropertyValue('background-color'); + + if (this.wavesurfer) { + this.wavesurfer.destroy(); + } + + const wavesurfer = WaveSurfer.create({ + container: this.waveform, + height, + barWidth: 3, + cursorWidth: 0, + progressColor, + waveColor, + forceDecode: true, + }); + + wavesurfer.setVolume(this.state.volume); + + if (preload) { + wavesurfer.load(src); + } else { + wavesurfer.load(src, arrayOf(1, 0.5), null, duration); + } + + wavesurfer.on('ready', () => this.setState({ duration: Math.floor(wavesurfer.getDuration()) })); + wavesurfer.on('audioprocess', () => this.setState({ currentTime: Math.floor(wavesurfer.getCurrentTime()) })); + wavesurfer.on('pause', () => this.setState({ paused: true })); + wavesurfer.on('play', () => this.setState({ paused: false })); + wavesurfer.on('volume', volume => this.setState({ volume })); + wavesurfer.on('mute', muted => this.setState({ muted })); + + this.wavesurfer = wavesurfer; + } + + togglePlay = () => { + if (this.state.paused) { + if (!this.props.preload) { + this.wavesurfer.createBackend(); + this.wavesurfer.createPeakCache(); + this.wavesurfer.load(this.props.src); + } + + this.wavesurfer.play(); + } else { + this.wavesurfer.pause(); + } + } + + toggleMute = () => { + this.wavesurfer.setMute(!this.state.muted); + } + + handleVolumeMouseDown = e => { + document.addEventListener('mousemove', this.handleMouseVolSlide, true); + document.addEventListener('mouseup', this.handleVolumeMouseUp, true); + document.addEventListener('touchmove', this.handleMouseVolSlide, true); + document.addEventListener('touchend', this.handleVolumeMouseUp, true); + + this.handleMouseVolSlide(e); + + e.preventDefault(); + e.stopPropagation(); + } + + handleVolumeMouseUp = () => { + document.removeEventListener('mousemove', this.handleMouseVolSlide, true); + document.removeEventListener('mouseup', this.handleVolumeMouseUp, true); + document.removeEventListener('touchmove', this.handleMouseVolSlide, true); + document.removeEventListener('touchend', this.handleVolumeMouseUp, true); + } + + handleMouseVolSlide = throttle(e => { + const rect = this.volume.getBoundingClientRect(); + const x = (e.clientX - rect.left) / this.volWidth; // x position within the element. + + if(!isNaN(x)) { + let slideamt = x; + + if (x > 1) { + slideamt = 1; + } else if(x < 0) { + slideamt = 0; + } + + this.wavesurfer.setVolume(slideamt); + } + }, 60); + + render () { + const { height, intl, alt, editable } = this.props; + const { paused, muted, volume, currentTime } = this.state; + + const volumeWidth = muted ? 0 : volume * this.volWidth; + const volumeHandleLoc = muted ? this.volHandleOffset(0) : this.volHandleOffset(volume); + + return ( +
+
+
+ +
+ +
+
+
+ + + +
+
+ + +
+ + + {formatTime(currentTime)} + / + {formatTime(this.state.duration || Math.floor(this.props.duration))} + +
+
+
+
+ ); + } + +} 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 873ea35fb..b676410d6 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -11,6 +11,7 @@ import { FormattedDate, FormattedNumber } from 'react-intl'; import Card from './card'; import ImmutablePureComponent from 'react-immutable-pure-component'; import Video from 'flavours/glitch/features/video'; +import Audio from 'flavours/glitch/features/audio'; import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon'; import scheduleIdleTask from 'flavours/glitch/util/schedule_idle_task'; import classNames from 'classnames'; @@ -131,7 +132,20 @@ export default class DetailedStatus extends ImmutablePureComponent { } else if (status.get('media_attachments').size > 0) { if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { media = ; - } else if (['video', 'audio'].includes(status.getIn(['media_attachments', 0, 'type']))) { + } else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') { + const attachment = status.getIn(['media_attachments', 0]); + + media = ( +