diff --git a/app/javascript/gabsocial/actions/interactions.js b/app/javascript/gabsocial/actions/interactions.js index bfa79fcd..86e6f085 100644 --- a/app/javascript/gabsocial/actions/interactions.js +++ b/app/javascript/gabsocial/actions/interactions.js @@ -2,25 +2,25 @@ import api from '../api'; import { importFetchedAccounts, importFetchedStatus } from './importer'; import { me } from '../initial_state'; -export const REBLOG_REQUEST = 'REBLOG_REQUEST'; -export const REBLOG_SUCCESS = 'REBLOG_SUCCESS'; -export const REBLOG_FAIL = 'REBLOG_FAIL'; +export const REPOST_REQUEST = 'REPOST_REQUEST'; +export const REPOST_SUCCESS = 'REPOST_SUCCESS'; +export const REPOST_FAIL = 'REPOST_FAIL'; export const FAVORITE_REQUEST = 'FAVORITE_REQUEST'; export const FAVORITE_SUCCESS = 'FAVORITE_SUCCESS'; export const FAVORITE_FAIL = 'FAVORITE_FAIL'; -export const UNREBLOG_REQUEST = 'UNREBLOG_REQUEST'; -export const UNREBLOG_SUCCESS = 'UNREBLOG_SUCCESS'; -export const UNREBLOG_FAIL = 'UNREBLOG_FAIL'; +export const UNREPOST_REQUEST = 'UNREPOST_REQUEST'; +export const UNREPOST_SUCCESS = 'UNREPOST_SUCCESS'; +export const UNREPOST_FAIL = 'UNREPOST_FAIL'; export const UNFAVORITE_REQUEST = 'UNFAVORITE_REQUEST'; export const UNFAVORITE_SUCCESS = 'UNFAVORITE_SUCCESS'; export const UNFAVORITE_FAIL = 'UNFAVORITE_FAIL'; -export const REBLOGS_FETCH_REQUEST = 'REBLOGS_FETCH_REQUEST'; -export const REBLOGS_FETCH_SUCCESS = 'REBLOGS_FETCH_SUCCESS'; -export const REBLOGS_FETCH_FAIL = 'REBLOGS_FETCH_FAIL'; +export const REPOSTS_FETCH_REQUEST = 'REPOSTS_FETCH_REQUEST'; +export const REPOSTS_FETCH_SUCCESS = 'REPOSTS_FETCH_SUCCESS'; +export const REPOSTS_FETCH_FAIL = 'REPOSTS_FETCH_FAIL'; export const PIN_REQUEST = 'PIN_REQUEST'; export const PIN_SUCCESS = 'PIN_SUCCESS'; @@ -30,119 +30,119 @@ export const UNPIN_REQUEST = 'UNPIN_REQUEST'; export const UNPIN_SUCCESS = 'UNPIN_SUCCESS'; export const UNPIN_FAIL = 'UNPIN_FAIL'; -export function reblog(status) { +export function repost(status) { return function (dispatch, getState) { if (!me) return; - dispatch(reblogRequest(status)); + dispatch(repostRequest(status)); api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`).then(function (response) { // The reblog API method returns a new status wrapped around the original. In this case we are only // interested in how the original is modified, hence passing it skipping the wrapper dispatch(importFetchedStatus(response.data.reblog)); - dispatch(reblogSuccess(status)); + dispatch(repostSuccess(status)); }).catch(function (error) { - dispatch(reblogFail(status, error)); + dispatch(repostFail(status, error)); }); }; }; -export function unreblog(status) { +export function unrepost(status) { return (dispatch, getState) => { if (!me) return; - dispatch(unreblogRequest(status)); + dispatch(unrepostRequest(status)); api(getState).post(`/api/v1/statuses/${status.get('id')}/unreblog`).then(response => { dispatch(importFetchedStatus(response.data)); - dispatch(unreblogSuccess(status)); + dispatch(unrepostSuccess(status)); }).catch(error => { - dispatch(unreblogFail(status, error)); + dispatch(unrepostFail(status, error)); }); }; }; -export function reblogRequest(status) { +export function repostRequest(status) { return { - type: REBLOG_REQUEST, + type: REPOST_REQUEST, status: status, skipLoading: true, }; }; -export function reblogSuccess(status) { +export function repostSuccess(status) { return { - type: REBLOG_SUCCESS, + type: REPOST_SUCCESS, status: status, skipLoading: true, }; }; -export function reblogFail(status, error) { +export function repostFail(status, error) { return { - type: REBLOG_FAIL, + type: REPOST_FAIL, status: status, error: error, skipLoading: true, }; }; -export function unreblogRequest(status) { +export function unrepostRequest(status) { return { - type: UNREBLOG_REQUEST, + type: UNREPOST_REQUEST, status: status, skipLoading: true, }; }; -export function unreblogSuccess(status) { +export function unrepostSuccess(status) { return { - type: UNREBLOG_SUCCESS, + type: UNREPOST_SUCCESS, status: status, skipLoading: true, }; }; -export function unreblogFail(status, error) { +export function unrepostFail(status, error) { return { - type: UNREBLOG_FAIL, + type: UNREPOST_FAIL, status: status, error: error, skipLoading: true, }; }; -export function favourite(status) { +export function favorite(status) { return function (dispatch, getState) { if (!me) return; - dispatch(favouriteRequest(status)); + dispatch(favoriteRequest(status)); api(getState).post(`/api/v1/statuses/${status.get('id')}/favourite`).then(function (response) { dispatch(importFetchedStatus(response.data)); - dispatch(favouriteSuccess(status)); + dispatch(favoriteSuccess(status)); }).catch(function (error) { - dispatch(favouriteFail(status, error)); + dispatch(favoriteFail(status, error)); }); }; }; -export function unfavourite(status) { +export function unfavorite(status) { return (dispatch, getState) => { if (!me) return; - dispatch(unfavouriteRequest(status)); + dispatch(unfavoriteRequest(status)); api(getState).post(`/api/v1/statuses/${status.get('id')}/unfavourite`).then(response => { dispatch(importFetchedStatus(response.data)); - dispatch(unfavouriteSuccess(status)); + dispatch(unfavoriteSuccess(status)); }).catch(error => { - dispatch(unfavouriteFail(status, error)); + dispatch(unfavoriteFail(status, error)); }); }; }; -export function favouriteRequest(status) { +export function favoriteRequest(status) { return { type: FAVORITE_REQUEST, status: status, @@ -150,7 +150,7 @@ export function favouriteRequest(status) { }; }; -export function favouriteSuccess(status) { +export function favoriteSuccess(status) { return { type: FAVORITE_SUCCESS, status: status, @@ -158,7 +158,7 @@ export function favouriteSuccess(status) { }; }; -export function favouriteFail(status, error) { +export function favoriteFail(status, error) { return { type: FAVORITE_FAIL, status: status, @@ -167,7 +167,7 @@ export function favouriteFail(status, error) { }; }; -export function unfavouriteRequest(status) { +export function unfavoriteRequest(status) { return { type: UNFAVORITE_REQUEST, status: status, @@ -175,7 +175,7 @@ export function unfavouriteRequest(status) { }; }; -export function unfavouriteSuccess(status) { +export function unfavoriteSuccess(status) { return { type: UNFAVORITE_SUCCESS, status: status, @@ -183,7 +183,7 @@ export function unfavouriteSuccess(status) { }; }; -export function unfavouriteFail(status, error) { +export function unfavoriteFail(status, error) { return { type: UNFAVORITE_FAIL, status: status, @@ -192,39 +192,39 @@ export function unfavouriteFail(status, error) { }; }; -export function fetchReblogs(id) { +export function fetchReposts(id) { return (dispatch, getState) => { if (!me) return; - dispatch(fetchReblogsRequest(id)); + dispatch(fetchRepostsRequest(id)); api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => { dispatch(importFetchedAccounts(response.data)); - dispatch(fetchReblogsSuccess(id, response.data)); + dispatch(fetchRepostsSuccess(id, response.data)); }).catch(error => { - dispatch(fetchReblogsFail(id, error)); + dispatch(fetchRepostsFail(id, error)); }); }; }; -export function fetchReblogsRequest(id) { +export function fetchRepostsRequest(id) { return { - type: REBLOGS_FETCH_REQUEST, + type: REPOSTS_FETCH_REQUEST, id, }; }; -export function fetchReblogsSuccess(id, accounts) { +export function fetchRepostsSuccess(id, accounts) { return { - type: REBLOGS_FETCH_SUCCESS, + type: REPOSTS_FETCH_SUCCESS, id, accounts, }; }; -export function fetchReblogsFail(id, error) { +export function fetchRepostsFail(id, error) { return { - type: REBLOGS_FETCH_FAIL, + type: REPOSTS_FETCH_FAIL, error, }; }; diff --git a/app/javascript/gabsocial/actions/timelines.js b/app/javascript/gabsocial/actions/timelines.js index 78f372b2..67e60feb 100644 --- a/app/javascript/gabsocial/actions/timelines.js +++ b/app/javascript/gabsocial/actions/timelines.js @@ -2,18 +2,18 @@ import { importFetchedStatus, importFetchedStatuses } from './importer'; import api, { getLinks } from '../api'; import { Map as ImmutableMap, List as ImmutableList, toJS } from 'immutable'; -export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; -export const TIMELINE_DELETE = 'TIMELINE_DELETE'; -export const TIMELINE_CLEAR = 'TIMELINE_CLEAR'; +export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; +export const TIMELINE_DELETE = 'TIMELINE_DELETE'; +export const TIMELINE_CLEAR = 'TIMELINE_CLEAR'; export const TIMELINE_UPDATE_QUEUE = 'TIMELINE_UPDATE_QUEUE'; export const TIMELINE_DEQUEUE = 'TIMELINE_DEQUEUE'; export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP'; 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_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL'; -export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; +export const TIMELINE_CONNECT = 'TIMELINE_CONNECT'; export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT'; export const MAX_QUEUED_ITEMS = 40; @@ -94,9 +94,9 @@ export function dequeueTimeline(timeline, expandFunc, optionalExpandArgs) { export function deleteFromTimelines(id) { return (dispatch, getState) => { - const accountId = getState().getIn(['statuses', id, 'account']); + const accountId = getState().getIn(['statuses', id, 'account']); const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => [status.get('id'), status.get('account')]); - const reblogOf = getState().getIn(['statuses', id, 'reblog'], null); + const reblogOf = getState().getIn(['statuses', id, 'reblog'], null); dispatch({ type: TIMELINE_DELETE, @@ -114,7 +114,7 @@ export function clearTimeline(timeline) { }; }; -const noOp = () => {}; +const noOp = () => { }; const parseTags = (tags = {}, mode) => { return (tags[mode] || []).map((tag) => { @@ -152,20 +152,20 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { }; }; -export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done); -export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { 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 expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); +export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, done); +export const expandPublicTimeline = ({ maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`public${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { 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 expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId }); 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); -export const expandGroupTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`group:${id}`, `/api/v1/timelines/group/${id}`, { max_id: maxId }, done); -export const expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => { +export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 20 }); +export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); +export const expandGroupTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`group:${id}`, `/api/v1/timelines/group/${id}`, { max_id: maxId }, done); +export const expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => { return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId, - any: parseTags(tags, 'any'), - all: parseTags(tags, 'all'), - none: parseTags(tags, 'none'), + any: parseTags(tags, 'any'), + all: parseTags(tags, 'all'), + none: parseTags(tags, 'none'), }, done); }; diff --git a/app/javascript/gabsocial/components/media_item.js b/app/javascript/gabsocial/components/media_item.js new file mode 100644 index 00000000..34928b31 --- /dev/null +++ b/app/javascript/gabsocial/components/media_item.js @@ -0,0 +1,132 @@ +import ImmutablePropTypes from 'react-immutable-proptypes' +import ImmutablePureComponent from 'react-immutable-pure-component' +import { NavLink } from 'react-router-dom' +import { decode } from 'blurhash' +import { autoPlayGif, displayMedia } from '../initial_state' +import Icon from './icon' +import Image from './image' +import Text from './text' + +export default class MediaItem extends ImmutablePureComponent { + + static propTypes = { + attachment: ImmutablePropTypes.map.isRequired, + } + + state = { + visible: displayMedia !== 'hide_all' && !this.props.attachment.getIn(['status', 'sensitive']) || displayMedia === 'show_all', + loaded: false, + } + + componentDidMount() { + if (this.props.attachment.get('blurhash')) { + this._decode() + } + } + + componentDidUpdate(prevProps) { + if (prevProps.attachment.get('blurhash') !== this.props.attachment.get('blurhash') && this.props.attachment.get('blurhash')) { + this._decode() + } + } + + _decode() { + const hash = this.props.attachment.get('blurhash') + const pixels = decode(hash, 160, 160) + + if (pixels && this.canvas) { + const ctx = this.canvas.getContext('2d') + const imageData = new ImageData(pixels, 160, 160) + + ctx.putImageData(imageData, 0, 0) + } + } + + setCanvasRef = c => { + this.canvas = c + } + + handleImageLoad = () => { + this.setState({ loaded: true }) + } + + hoverToPlay() { + return !autoPlayGif && ['gifv', 'video'].indexOf(this.props.attachment.get('type')) !== -1 + } + + render() { + const { attachment } = this.props + const { visible, loaded } = this.state + + const status = attachment.get('status') + const title = status.get('spoiler_text') || attachment.get('description') + + const attachmentType = attachment.get('type') + let badge = null + + if (attachmentType === 'video') { + const duration = attachment.getIn(['meta', 'duration']) + badge = (duration / 60).toFixed(2) + } else if (attachmentType === 'gifv') { + badge = 'GIF' + } + + return ( +
+
+ + { + (!loaded || !visible) && + + } + + { + visible && + {attachment.get('description')} + } + +
+ { + !visible && + + } + + { + !!badge && +
+ + {badge} + +
+ } + +
+ +
+
+
+ ) + } + +} diff --git a/app/javascript/gabsocial/components/modal/boost_modal.js b/app/javascript/gabsocial/components/modal/boost_modal.js index 515b4f54..26cb63b1 100644 --- a/app/javascript/gabsocial/components/modal/boost_modal.js +++ b/app/javascript/gabsocial/components/modal/boost_modal.js @@ -24,7 +24,7 @@ class BoostModal extends ImmutablePureComponent { static propTypes = { status: ImmutablePropTypes.map.isRequired, - onReblog: PropTypes.func.isRequired, + onRepost: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; @@ -33,8 +33,8 @@ class BoostModal extends ImmutablePureComponent { this.button.focus(); } - handleReblog = () => { - this.props.onReblog(this.props.status); + handleRepost = () => { + this.props.onRepost(this.props.status); this.props.onClose(); } @@ -96,7 +96,7 @@ class BoostModal extends ImmutablePureComponent { } })} -