This commit is contained in:
mgabdev 2020-04-01 23:17:21 -04:00
parent 1a33759e19
commit 80d41b8d94
50 changed files with 1771 additions and 610 deletions

View File

@ -12,7 +12,9 @@ class Api::V1::GroupsController < Api::BaseController
def index def index
case current_tab case current_tab
when 'featured' when 'featured'
@groups = Group.where(is_featured: true).limit(50).all @groups = Group.where(is_featured: true, is_archived: false).limit(50).all
when 'new'
@groups = Group.where(is_archived: false).limit(24).order('created_at DESC').all
when 'member' when 'member'
@groups = Group.joins(:group_accounts).where(is_archived: false, group_accounts: { account: current_account }).order('group_accounts.unread_count DESC, group_accounts.id DESC').all @groups = Group.joins(:group_accounts).where(is_archived: false, group_accounts: { account: current_account }).order('group_accounts.unread_count DESC, group_accounts.id DESC').all
when 'admin' when 'admin'
@ -24,7 +26,7 @@ class Api::V1::GroupsController < Api::BaseController
def current_tab def current_tab
tab = 'featured' tab = 'featured'
tab = params[:tab] if ['featured', 'member', 'admin'].include? params[:tab] tab = params[:tab] if ['featured', 'member', 'admin', 'new'].include? params[:tab]
return tab return tab
end end

View File

@ -607,8 +607,9 @@ export function changeScheduledAt(date) {
}; };
}; };
export function changeRichTextEditorControlsVisibility() { export function changeRichTextEditorControlsVisibility(status) {
return { return {
type: COMPOSE_RICH_TEXT_EDITOR_CONTROLS_VISIBILITY, type: COMPOSE_RICH_TEXT_EDITOR_CONTROLS_VISIBILITY,
status: status,
} }
} }

View File

@ -0,0 +1,220 @@
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_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_CONNECT = 'TIMELINE_CONNECT';
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
export const MAX_QUEUED_ITEMS = 40;
export function updateTimeline(timeline, status, accept) {
return dispatch => {
if (typeof accept === 'function' && !accept(status)) {
return;
}
dispatch(importFetchedStatus(status));
dispatch({
type: TIMELINE_UPDATE,
timeline,
status,
});
};
};
export function updateTimelineQueue(timeline, status, accept) {
return dispatch => {
if (typeof accept === 'function' && !accept(status)) {
return;
}
dispatch({
type: TIMELINE_UPDATE_QUEUE,
timeline,
status,
});
}
};
export function dequeueTimeline(timeline, expandFunc, optionalExpandArgs) {
return (dispatch, getState) => {
const queuedItems = getState().getIn(['timelines', timeline, 'queuedItems'], ImmutableList());
const totalQueuedItemsCount = getState().getIn(['timelines', timeline, 'totalQueuedItemsCount'], 0);
let shouldDispatchDequeue = true;
if (totalQueuedItemsCount == 0) {
return;
}
else if (totalQueuedItemsCount > 0 && totalQueuedItemsCount <= MAX_QUEUED_ITEMS) {
queuedItems.forEach(status => {
dispatch(updateTimeline(timeline, status.toJS(), null));
});
}
else {
if (typeof expandFunc === 'function') {
dispatch(clearTimeline(timeline));
expandFunc();
}
else {
if (timeline === 'home') {
dispatch(clearTimeline(timeline));
dispatch(expandHomeTimeline(optionalExpandArgs));
}
else if (timeline === 'community') {
dispatch(clearTimeline(timeline));
dispatch(expandCommunityTimeline(optionalExpandArgs));
}
else {
shouldDispatchDequeue = false;
}
}
}
if (!shouldDispatchDequeue) return;
dispatch({
type: TIMELINE_DEQUEUE,
timeline,
});
}
};
export function deleteFromTimelines(id) {
return (dispatch, getState) => {
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);
dispatch({
type: TIMELINE_DELETE,
id,
accountId,
references,
reblogOf,
});
};
};
export function clearTimeline(timeline) {
return (dispatch) => {
dispatch({ type: TIMELINE_CLEAR, timeline });
};
};
const noOp = () => { };
const parseTags = (tags = {}, mode) => {
return (tags[mode] || []).map((tag) => {
return tag.value;
});
};
export function expandTimeline(timelineId, path, params = {}, done = noOp) {
return (dispatch, getState) => {
const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
const isLoadingMore = !!params.max_id;
if (timeline.get('isLoading')) {
done();
return;
}
if (!params.max_id && !params.pinned && timeline.get('items', ImmutableList()).size > 0) {
params.since_id = timeline.getIn(['items', 0]);
}
const isLoadingRecent = !!params.since_id;
dispatch(expandTimelineRequest(timelineId, isLoadingMore));
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));
done();
}).catch(error => {
dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
done();
});
};
};
export const expandHomeTimeline = ({ maxId } = {}, done = noOp) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId }, 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, limit } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 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'),
}, done);
};
export function expandTimelineRequest(timeline, isLoadingMore) {
return {
type: TIMELINE_EXPAND_REQUEST,
timeline,
skipLoading: !isLoadingMore,
};
};
export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingRecent, isLoadingMore) {
return {
type: TIMELINE_EXPAND_SUCCESS,
timeline,
statuses,
next,
partial,
isLoadingRecent,
skipLoading: !isLoadingMore,
};
};
export function expandTimelineFail(timeline, error, isLoadingMore) {
return {
type: TIMELINE_EXPAND_FAIL,
timeline,
error,
skipLoading: !isLoadingMore,
};
};
export function connectTimeline(timeline) {
return {
type: TIMELINE_CONNECT,
timeline,
};
};
export function disconnectTimeline(timeline) {
return {
type: TIMELINE_DISCONNECT,
timeline,
};
};
export function scrollTopTimeline(timeline, top) {
return {
type: TIMELINE_SCROLL_TOP,
timeline,
top,
};
};

View File

@ -0,0 +1,104 @@
import axios from 'axios'
import { me, tenorkey } from '../initial_state'
export const GIFS_CLEAR_RESULTS = 'GIFS_CLEAR_RESULTS'
export const GIF_SET_SELECTED = 'GIF_SET_SELECTED'
export const GIF_CHANGE_SEARCH_TEXT = 'GIF_CHANGE_SEARCH_TEXT'
export const GIF_RESULTS_FETCH_REQUEST = 'GIF_RESULTS_FETCH_REQUEST'
export const GIF_RESULTS_FETCH_SUCCESS = 'GIF_RESULTS_FETCH_SUCCESS'
export const GIF_RESULTS_FETCH_FAIL = 'GIF_RESULTS_FETCH_FAIL'
export const GIF_CATEGORIES_FETCH_REQUEST = 'GIF_CATEGORIES_FETCH_REQUEST'
export const GIF_CATEGORIES_FETCH_SUCCESS = 'GIF_CATEGORIES_FETCH_SUCCESS'
export const GIF_CATEGORIES_FETCH_FAIL = 'GIF_CATEGORIES_FETCH_FAIL'
export const fetchGifCategories = () => {
return function (dispatch) {
if (!me) return
dispatch(fetchGifCategoriesRequest())
axios.get(`https://api.tenor.com/v1/categories?media_filter=minimal&limit=30&key=${tenorkey}`)
.then((response) => {
console.log("fetchGifCategoriesSuccess:", response)
dispatch(fetchGifCategoriesSuccess(response.data.tags))
}).catch(function (error) {
dispatch(fetchGifCategoriesFail(error))
})
}
}
export const fetchGifResults = () => {
return function (dispatch, getState) {
if (!me) return
dispatch(fetchGifResultsRequest())
const searchText = getState().getIn(['tenor', 'searchText'], '');
axios.get(`https://api.tenor.com/v1/search?q=${searchText}&media_filter=minimal&limit=30&key=QHFJ0C5EWGBH`)
.then((response) => {
console.log("response:", response)
dispatch(fetchGifResultsSuccess(response.data.results))
}).catch(function (error) {
dispatch(fetchGifResultsFail(error))
})
}
}
export const clearGifResults = () => ({
type: GIFS_CLEAR_RESULTS,
})
export const setSelectedGif = (url) => ({
type: GIF_SET_SELECTED,
url,
})
export function changeGifSearchText(text) {
return {
type: GIF_CHANGE_SEARCH_TEXT,
text,
}
}
function fetchGifResultsRequest() {
return {
type: GIF_RESULTS_FETCH_REQUEST,
}
}
function fetchGifResultsSuccess(results) {
return {
type: GIF_RESULTS_FETCH_SUCCESS,
results,
}
}
function fetchGifResultsFail(error) {
return {
type: GIF_RESULTS_FETCH_FAIL,
error,
}
}
function fetchGifCategoriesRequest() {
return {
type: GIF_CATEGORIES_FETCH_REQUEST,
}
}
function fetchGifCategoriesSuccess(categories) {
return {
type: GIF_CATEGORIES_FETCH_SUCCESS,
categories,
}
}
function fetchGifCategoriesFail(error) {
return {
type: GIF_CATEGORIES_FETCH_FAIL,
error,
}
}

View File

@ -0,0 +1,27 @@
const BlockQuoteIcon = ({
className = '',
width = '16px',
height = '16px',
viewBox = '0 0 34 32',
title = 'Block Quote',
}) => (
<svg
className={className}
version='1.1'
xmlns='http://www.w3.org/2000/svg'
x='0px'
y='0px'
width={width}
height={height}
viewBox={viewBox}
xmlSpace='preserve'
aria-label={title}
>
<g>
<path d='M 0 18.285156 L 6.855469 18.285156 L 2.285156 27.429688 L 9.144531 27.429688 L 13.714844 18.285156 L 13.714844 4.570312 L 0 4.570312 Z M 0 18.285156' />
<path d='M 18.285156 4.570312 L 18.285156 18.285156 L 25.144531 18.285156 L 20.570312 27.429688 L 27.429688 27.429688 L 32 18.285156 L 32 4.570312 Z M 18.285156 4.570312' />
</g>
</svg>
)
export default BlockQuoteIcon

View File

@ -0,0 +1,26 @@
const BoldIcon = ({
className = '',
width = '16px',
height = '16px',
viewBox = '0 0 34 32',
title = 'Strikethrough',
}) => (
<svg
className={className}
version='1.1'
xmlns='http://www.w3.org/2000/svg'
x='0px'
y='0px'
width={width}
height={height}
viewBox={viewBox}
xmlSpace='preserve'
aria-label={title}
>
<g>
<path d='M 23.371094 15.519531 C 25.578125 13.976562 27.144531 11.484375 27.144531 9.144531 C 27.144531 3.988281 23.152344 0 18 0 L 3.714844 0 L 3.714844 32 L 19.804688 32 C 24.59375 32 28.285156 28.113281 28.285156 23.335938 C 28.285156 19.863281 26.308594 16.902344 23.371094 15.519531 Z M 10.570312 5.714844 L 17.429688 5.714844 C 19.324219 5.714844 20.855469 7.246094 20.855469 9.144531 C 20.855469 11.039062 19.324219 12.570312 17.429688 12.570312 L 10.570312 12.570312 Z M 18.570312 26.285156 L 10.570312 26.285156 L 10.570312 19.429688 L 18.570312 19.429688 C 20.46875 19.429688 22 20.960938 22 22.855469 C 22 24.753906 20.46875 26.285156 18.570312 26.285156 Z M 18.570312 26.285156' />
</g>
</svg>
)
export default BoldIcon

View File

@ -0,0 +1,26 @@
const ItalicIcon = ({
className = '',
width = '16px',
height = '16px',
viewBox = '0 0 34 32',
title = 'Italic',
}) => (
<svg
className={className}
version='1.1'
xmlns='http://www.w3.org/2000/svg'
x='0px'
y='0px'
width={width}
height={height}
viewBox={viewBox}
xmlSpace='preserve'
aria-label={title}
>
<g>
<path d='M 11.429688 0 L 11.429688 6.855469 L 16.492188 6.855469 L 8.652344 25.144531 L 2.285156 25.144531 L 2.285156 32 L 20.570312 32 L 20.570312 25.144531 L 15.507812 25.144531 L 23.347656 6.855469 L 29.714844 6.855469 L 29.714844 0 Z M 11.429688 0' />
</g>
</svg>
)
export default ItalicIcon

View File

@ -0,0 +1,31 @@
const OLListIcon = ({
className = '',
width = '16px',
height = '16px',
viewBox = '0 0 34 32',
title = 'Ordered List',
}) => (
<svg
className={className}
version='1.1'
xmlns='http://www.w3.org/2000/svg'
x='0px'
y='0px'
width={width}
height={height}
viewBox={viewBox}
xmlSpace='preserve'
aria-label={title}
>
<g>
<path d='M 0 24.421875 L 3.367188 24.421875 L 3.367188 25.261719 L 1.683594 25.261719 L 1.683594 26.949219 L 3.367188 26.949219 L 3.367188 27.789062 L 0 27.789062 L 0 29.472656 L 5.050781 29.472656 L 5.050781 22.738281 L 0 22.738281 Z M 0 24.421875' />
<path d='M 1.683594 9.261719 L 3.367188 9.261719 L 3.367188 2.527344 L 0 2.527344 L 0 4.210938 L 1.683594 4.210938 Z M 1.683594 9.261719' />
<path d='M 0 14.316406 L 3.03125 14.316406 L 0 17.851562 L 0 19.367188 L 5.050781 19.367188 L 5.050781 17.683594 L 2.019531 17.683594 L 5.050781 14.148438 L 5.050781 12.632812 L 0 12.632812 Z M 0 14.316406' />
<path d='M 8.421875 24.421875 L 32 24.421875 L 32 27.789062 L 8.421875 27.789062 Z M 8.421875 24.421875' />
<path d='M 8.421875 4.210938 L 32 4.210938 L 32 7.578125 L 8.421875 7.578125 Z M 8.421875 4.210938' />
<path d='M 8.421875 14.316406 L 32 14.316406 L 32 17.683594 L 8.421875 17.683594 Z M 8.421875 14.316406' />
</g>
</svg>
)
export default OLListIcon

View File

@ -0,0 +1,27 @@
const StrikethroughIcon = ({
className = '',
width = '16px',
height = '16px',
viewBox = '0 0 34 32',
title = 'Strikethrough',
}) => (
<svg
className={className}
version='1.1'
xmlns='http://www.w3.org/2000/svg'
x='0px'
y='0px'
width={width}
height={height}
viewBox={viewBox}
xmlSpace='preserve'
aria-label={title}
>
<g>
<path d='M 7.527344 12.890625 C 7.617188 13.050781 7.714844 13.191406 7.804688 13.34375 L 16 13.34375 C 14.863281 12.949219 14.167969 12.535156 13.503906 12.097656 C 12.632812 11.519531 12.195312 10.800781 12.195312 9.945312 C 12.195312 9.539062 12.285156 9.136719 12.453125 8.773438 C 12.621094 8.410156 12.878906 8.089844 13.21875 7.8125 C 13.554688 7.539062 13.980469 7.324219 14.496094 7.171875 C 15.023438 7.023438 15.625 6.941406 16.328125 6.941406 C 17.058594 6.941406 17.6875 7.03125 18.222656 7.21875 C 18.753906 7.394531 19.199219 7.664062 19.554688 7.992188 C 19.910156 8.320312 20.179688 8.71875 20.347656 9.191406 C 20.515625 9.652344 20.605469 10.160156 20.605469 10.703125 L 25.957031 10.703125 C 25.957031 9.539062 25.734375 8.460938 25.28125 7.484375 C 24.828125 6.496094 24.1875 5.652344 23.351562 4.941406 C 22.515625 4.222656 21.511719 3.671875 20.335938 3.269531 C 19.164062 2.871094 17.859375 2.675781 16.417969 2.675781 C 15.011719 2.675781 13.734375 2.851562 12.558594 3.199219 C 11.386719 3.546875 10.382812 4.035156 9.527344 4.667969 C 8.675781 5.296875 8.019531 6.0625 7.546875 6.960938 C 7.074219 7.859375 6.84375 8.84375 6.84375 9.929688 C 6.84375 11.058594 7.074219 12.042969 7.527344 12.890625 Z M 7.527344 12.890625' />
<path d='M 0 15.121094 L 0 18.675781 L 17.109375 18.675781 C 17.429688 18.800781 17.820312 18.925781 18.089844 19.039062 C 18.746094 19.332031 19.261719 19.644531 19.636719 19.945312 C 20.007812 20.257812 20.257812 20.59375 20.390625 20.949219 C 20.523438 21.316406 20.585938 21.722656 20.585938 22.167969 C 20.585938 22.585938 20.507812 22.976562 20.347656 23.332031 C 20.1875 23.699219 19.9375 24.007812 19.609375 24.265625 C 19.28125 24.523438 18.851562 24.730469 18.347656 24.878906 C 17.832031 25.03125 17.234375 25.101562 16.542969 25.101562 C 15.769531 25.101562 15.066406 25.023438 14.4375 24.871094 C 13.804688 24.71875 13.269531 24.472656 12.828125 24.132812 C 12.382812 23.796875 12.035156 23.351562 11.785156 22.808594 C 11.539062 22.265625 11.332031 21.449219 11.332031 20.667969 L 6.042969 20.667969 C 6.042969 21.644531 6.1875 22.675781 6.460938 23.476562 C 6.738281 24.277344 7.128906 24.996094 7.617188 25.625 C 8.105469 26.257812 8.691406 26.800781 9.359375 27.261719 C 10.027344 27.722656 10.746094 28.117188 11.527344 28.417969 C 12.3125 28.730469 13.121094 28.960938 13.980469 29.101562 C 14.835938 29.253906 15.699219 29.324219 16.550781 29.324219 C 17.972656 29.324219 19.269531 29.164062 20.425781 28.835938 C 21.582031 28.507812 22.578125 28.035156 23.394531 27.429688 C 24.214844 26.816406 24.84375 26.070312 25.296875 25.171875 C 25.75 24.277344 25.964844 23.261719 25.964844 22.125 C 25.964844 21.058594 25.777344 20.097656 25.414062 19.253906 C 25.324219 19.050781 25.226562 18.851562 25.109375 18.65625 L 32 18.65625 L 32 15.121094 Z M 0 15.121094' />
</g>
</svg>
)
export default StrikethroughIcon

View File

@ -0,0 +1,27 @@
const TextSizeIcon = ({
className = '',
width = '16px',
height = '16px',
viewBox = '0 0 34 32',
title = 'Text Size',
}) => (
<svg
className={className}
version='1.1'
xmlns='http://www.w3.org/2000/svg'
x='0px'
y='0px'
width={width}
height={height}
viewBox={viewBox}
xmlSpace='preserve'
aria-label={title}
>
<g>
<path d='M 0 16.84375 L 5.050781 16.84375 L 5.050781 28.632812 L 10.105469 28.632812 L 10.105469 16.84375 L 15.15625 16.84375 L 15.15625 11.789062 L 0 11.789062 Z M 0 16.84375 '/>
<path d='M 10.105469 3.367188 L 10.105469 8.421875 L 18.527344 8.421875 L 18.527344 28.632812 L 23.578125 28.632812 L 23.578125 8.421875 L 32 8.421875 L 32 3.367188 Z M 10.105469 3.367188 '/>
</g>
</svg>
)
export default TextSizeIcon

View File

@ -0,0 +1,31 @@
const ULListIcon = ({
className = '',
width = '16px',
height = '16px',
viewBox = '0 0 34 32',
title = 'Unordered List',
}) => (
<svg
className={className}
version='1.1'
xmlns='http://www.w3.org/2000/svg'
x='0px'
y='0px'
width={width}
height={height}
viewBox={viewBox}
xmlSpace='preserve'
aria-label={title}
>
<g>
<path d='M 2.59375 3.027344 C 1.160156 3.027344 0 4.1875 0 5.621094 C 0 7.058594 1.160156 8.214844 2.59375 8.214844 C 4.03125 8.214844 5.1875 7.058594 5.1875 5.621094 C 5.1875 4.1875 4.03125 3.027344 2.59375 3.027344 Z M 2.59375 3.027344' />
<path d='M 2.59375 13.40625 C 1.160156 13.40625 0 14.5625 0 16 C 0 17.4375 1.160156 18.59375 2.59375 18.59375 C 4.03125 18.59375 5.1875 17.4375 5.1875 16 C 5.1875 14.5625 4.03125 13.40625 2.59375 13.40625 Z M 2.59375 13.40625' />
<path d='M 2.59375 23.785156 C 1.148438 23.785156 0 24.953125 0 26.378906 C 0 27.804688 1.167969 28.972656 2.59375 28.972656 C 4.023438 28.972656 5.1875 27.804688 5.1875 26.378906 C 5.1875 24.953125 4.039062 23.785156 2.59375 23.785156 Z M 2.59375 23.785156' />
<path d='M 7.785156 24.648438 L 32 24.648438 L 32 28.109375 L 7.785156 28.109375 Z M 7.785156 24.648438' />
<path d='M 7.785156 3.890625 L 32 3.890625 L 32 7.351562 L 7.785156 7.351562 Z M 7.785156 3.890625' />
<path d='M 7.785156 14.269531 L 32 14.269531 L 32 17.730469 L 7.785156 17.730469 Z M 7.785156 14.269531' />
</g>
</svg>
)
export default ULListIcon

View File

@ -0,0 +1,27 @@
const UnderlineIcon = ({
className = '',
width = '16px',
height = '16px',
viewBox = '0 0 34 32',
title = 'Underline',
}) => (
<svg
className={className}
version='1.1'
xmlns='http://www.w3.org/2000/svg'
x='0px'
y='0px'
width={width}
height={height}
viewBox={viewBox}
xmlSpace='preserve'
aria-label={title}
>
<g>
<path d='M 16 24.890625 C 21.894531 24.890625 26.667969 20.117188 26.667969 14.222656 L 26.667969 0 L 22.222656 0 L 22.222656 14.222656 C 22.222656 17.664062 19.441406 20.445312 16 20.445312 C 12.558594 20.445312 9.777344 17.664062 9.777344 14.222656 L 9.777344 0 L 5.332031 0 L 5.332031 14.222656 C 5.332031 20.117188 10.105469 24.890625 16 24.890625 Z M 16 24.890625' />
<path d='M 3.554688 28.445312 L 28.445312 28.445312 L 28.445312 32 L 3.554688 32 Z M 3.554688 28.445312' />
</g>
</svg>
)
export default UnderlineIcon

View File

@ -36,6 +36,7 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
onBlur: PropTypes.func, onBlur: PropTypes.func,
textarea: PropTypes.bool, textarea: PropTypes.bool,
small: PropTypes.bool, small: PropTypes.bool,
prependIcon: PropTypes.string,
} }
static defaultProps = { static defaultProps = {
@ -208,7 +209,8 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
className, className,
id, id,
maxLength, maxLength,
textarea textarea,
prependIcon
} = this.props } = this.props
const { suggestionsHidden } = this.state const { suggestionsHidden } = this.state
@ -232,26 +234,11 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
mr5: small, mr5: small,
}) })
// <div aria-activedescendant="typeaheadFocus-0.35973815699338085"
// aria-autocomplete="list"
// aria-controls="typeaheadDropdownWrapped-0"
// aria-describedby="placeholder-7g4r6"
// aria-label="Tweet text"
// aria-multiline="true"
// class="notranslate public-DraftEditor-content"
// contenteditable="true"
// data-testid="tweetTextarea_0"
// role="textbox"
// spellcheck="true"
// tabindex="0"
// no-focuscontainer-refocus="true"
// style="outline: none; user-select: text; white-space: pre-wrap; overflow-wrap: break-word;">
if (textarea) { if (textarea) {
return ( return (
<Fragment> <Fragment>
<div className={[_s.default, _s.flexGrow1].join(' ')}> <div className={[_s.default, _s.flexGrow1].join(' ')}>
<div className={[_s.default, _s.ml5].join(' ')}> <div className={[_s.default].join(' ')}>
<Composer <Composer
inputRef={this.setTextbox} inputRef={this.setTextbox}
@ -265,6 +252,7 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
onFocus={this.onFocus} onFocus={this.onFocus}
onBlur={this.onBlur} onBlur={this.onBlur}
onPaste={this.onPaste} onPaste={this.onPaste}
small={small}
/> />
{ /* <Textarea { /* <Textarea
@ -350,6 +338,7 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
id={id} id={id}
className={className} className={className}
maxLength={maxLength} maxLength={maxLength}
prependIcon={prependIcon}
/> />
</label> </label>

View File

@ -1,35 +0,0 @@
import Text from './text'
export default class Badge extends PureComponent {
static propTypes = {
children: PropTypes.string,
description: PropTypes.string,
}
state = {
hovering: false,
}
handleOnMouseEnter = () => {
this.setState({ hovering: true })
}
handleOnMouseLeave = () => {
this.setState({ hovering: false })
}
render() {
const { children, description } = this.props
const { hovering } = this.state // : todo : tooltip
return (
<Text
color='white'
size='extraSmall'
className={[_s.backgroundColorBrand, _s.px5, _s.lineHeight125, _s.radiusSmall].join(' ')}
>
{children}
</Text>
)
}
}

View File

@ -6,8 +6,12 @@ import {
} from 'draft-js' } from 'draft-js'
import { urlRegex } from '../features/compose/util/url_regex' import { urlRegex } from '../features/compose/util/url_regex'
import classNames from 'classnames/bind' import classNames from 'classnames/bind'
import { me } from '../initial_state'
import { makeGetAccount } from '../selectors'
import Button from './button' import Button from './button'
import 'draft-js/dist/Draft.css'
const cx = classNames.bind(_s) const cx = classNames.bind(_s)
const getBlockStyle = (block) => { const getBlockStyle = (block) => {
@ -57,55 +61,61 @@ const RTE_ITEMS = [
label: 'Bold', label: 'Bold',
style: 'BOLD', style: 'BOLD',
type: 'style', type: 'style',
icon: 'circle', icon: 'bold',
}, },
{ {
label: 'Italic', label: 'Italic',
style: 'ITALIC', style: 'ITALIC',
type: 'style', type: 'style',
icon: 'circle', icon: 'italic',
}, },
{ {
label: 'Underline', label: 'Underline',
style: 'UNDERLINE', style: 'UNDERLINE',
type: 'style', type: 'style',
icon: 'circle', icon: 'underline',
}, },
{ {
label: 'Monospace', label: 'Strikethrough',
style: 'CODE', style: 'STRIKETHROUGH',
type: 'style', type: 'style',
icon: 'circle', icon: 'strikethrough',
}, },
// {
// label: 'Monospace',
// style: 'CODE',
// type: 'style',
// icon: 'circle',
// },
{ {
label: 'H1', label: 'H1',
style: 'header-one', style: 'header-one',
type: 'block', type: 'block',
icon: 'circle', icon: 'text-size',
}, },
{ {
label: 'Blockquote', label: 'Blockquote',
style: 'blockquote', style: 'blockquote',
type: 'block', type: 'block',
icon: 'circle', icon: 'blockquote',
},
{
label: 'UL',
style: 'unordered-list-item',
type: 'block',
icon: 'circle',
},
{
label: 'OL',
style: 'ordered-list-item',
type: 'block',
icon: 'circle',
}, },
{ {
label: 'Code Block', label: 'Code Block',
style: 'code-block', style: 'code-block',
type: 'block', type: 'block',
icon: 'circle', icon: 'code',
},
{
label: 'UL',
style: 'unordered-list-item',
type: 'block',
icon: 'ul-list',
},
{
label: 'OL',
style: 'ordered-list-item',
type: 'block',
icon: 'ol-list',
}, },
] ]
@ -127,7 +137,26 @@ const compositeDecorator = new CompositeDecorator([
const HANDLE_REGEX = /\@[\w]+/g; const HANDLE_REGEX = /\@[\w]+/g;
const HASHTAG_REGEX = /\#[\w\u0590-\u05ff]+/g; const HASHTAG_REGEX = /\#[\w\u0590-\u05ff]+/g;
export default class Composer extends PureComponent { const mapStateToProps = state => {
const getAccount = makeGetAccount()
const account = getAccount(state, me)
const isPro = account.get('is_pro')
return {
isPro,
rteControlsVisible: state.getIn(['compose', 'rte_controls_visible']),
}
}
const mapDispatchToProps = dispatch => {
return {
}
}
export default
@connect(mapStateToProps, mapDispatchToProps)
class Composer extends PureComponent {
static propTypes = { static propTypes = {
inputRef: PropTypes.func, inputRef: PropTypes.func,
@ -141,6 +170,9 @@ export default class Composer extends PureComponent {
onFocus: PropTypes.func, onFocus: PropTypes.func,
onBlur: PropTypes.func, onBlur: PropTypes.func,
onPaste: PropTypes.func, onPaste: PropTypes.func,
small: PropTypes.bool,
isPro: PropTypes.bool.isRequired,
rteControlsVisible: PropTypes.bool.isRequired,
} }
state = { state = {
@ -176,22 +208,21 @@ export default class Composer extends PureComponent {
this.onChange(RichUtils.onTab(e, this.state.editorState, maxDepth)) this.onChange(RichUtils.onTab(e, this.state.editorState, maxDepth))
} }
toggleBlockType = (blockType) => { toggleEditorStyle = (style, type) => {
this.onChange( console.log("toggleEditorStyle:", style, type)
RichUtils.toggleBlockType( if (type === 'style') {
this.state.editorState, this.onChange(
blockType RichUtils.toggleInlineStyle(this.state.editorState, style)
) )
) } else if (type === 'block') {
this.onChange(
RichUtils.toggleBlockType(this.state.editorState, style)
)
}
} }
toggleInlineStyle = (inlineStyle) => { handleOnTogglePopoutEditor = () => {
this.onChange( //
RichUtils.toggleInlineStyle(
this.state.editorState,
inlineStyle
)
)
} }
setRef = (n) => { setRef = (n) => {
@ -210,28 +241,61 @@ export default class Composer extends PureComponent {
onKeyUp, onKeyUp,
onFocus, onFocus,
onBlur, onBlur,
onPaste onPaste,
small,
isPro,
rteControlsVisible
} = this.props } = this.props
const { editorState } = this.state const { editorState } = this.state
const editorContainerClasses = cx({
default: 1,
RTE: 1,
cursorText: 1,
text: 1,
fontSize16PX: !small,
fontSize14PX: small,
pt15: !small,
px15: !small,
px10: small,
pt10: small,
pb10: 1,
})
return ( return (
<div className={[_s.default].join(' ')}> <div className={[_s.default].join(' ')}>
<div className={[_s.default, _s.backgroundColorPrimary, _s.borderBottom1PX, _s.borderColorSecondary, _s.py5, _s.px15, _s.alignItemsCenter, _s.flexRow].join(' ')}> {
{ rteControlsVisible && isPro &&
RTE_ITEMS.map((item, i) => ( <div className={[_s.default, _s.backgroundColorPrimary, _s.borderBottom1PX, _s.borderColorSecondary, _s.py5, _s.px15, _s.alignItemsCenter, _s.flexRow].join(' ')}>
<StyleButton {
key={`rte-button-${i}`} RTE_ITEMS.map((item, i) => (
editorState={editorState} <StyleButton
{...item} key={`rte-button-${i}`}
/> editorState={editorState}
)) onClick={this.toggleEditorStyle}
} {...item}
</div> />
))
}
<Button
backgroundColor='none'
color='secondary'
onClick={this.handleOnTogglePopoutEditor}
title='Fullscreen'
className={[_s.px10, _s.noSelect, _s.marginLeftAuto].join(' ')}
icon='fullscreen'
iconClassName={_s.inheritFill}
iconWidth='12px'
iconHeight='12px'
radiusSmall
/>
</div>
}
<div <div
onClick={this.focus} onClick={this.focus}
className={[_s.text, _s.fontSize16PX].join(' ')} className={editorContainerClasses}
> >
<Editor <Editor
blockStyleFn={getBlockStyle} blockStyleFn={getBlockStyle}
@ -240,7 +304,7 @@ export default class Composer extends PureComponent {
handleKeyCommand={this.handleKeyCommand} handleKeyCommand={this.handleKeyCommand}
onChange={this.onChange} onChange={this.onChange}
onTab={this.onTab} onTab={this.onTab}
placeholder={placeholder} // placeholder={placeholder}
ref={this.setRef} ref={this.setRef}
/> />
</div> </div>
@ -252,17 +316,17 @@ export default class Composer extends PureComponent {
class StyleButton extends PureComponent { class StyleButton extends PureComponent {
static propTypes = { static propTypes = {
onToggle: PropTypes.func, onClick: PropTypes.func,
label: PropTypes.string, label: PropTypes.string,
style: PropTypes.string, style: PropTypes.string,
icon: PropTypes.string, icon: PropTypes.string,
type: PropTypes.string, type: PropTypes.string,
} }
handleOnToggle = (e) => { handleOnClick
const { onToggle, style } = this.props = (e) => {
e.preventDefault() e.preventDefault()
onToggle(style) this.props.onClick(this.props.style, this.props.type)
} }
render() { render() {
@ -279,13 +343,13 @@ class StyleButton extends PureComponent {
const currentStyle = editorState.getCurrentInlineStyle() const currentStyle = editorState.getCurrentInlineStyle()
const blockType = editorState.getCurrentContent().getBlockForKey(selection.getStartKey()).getType() const blockType = editorState.getCurrentContent().getBlockForKey(selection.getStartKey()).getType()
let active const active = type === 'block' ? style === blockType : currentStyle.has(style)
// active={type.style === blockType} const color = active ? 'white' : 'secondary'
// active={currentStyle.has(type.style)}
const btnClasses = cx({ const btnClasses = cx({
px10: 1, px10: 1,
mr5: 1, mr5: 1,
noSelect: 1,
backgroundSubtle2Dark_onHover: 1, backgroundSubtle2Dark_onHover: 1,
backgroundColorBrandLight: active, backgroundColorBrandLight: active,
// py10: !small, // py10: !small,
@ -293,23 +357,20 @@ class StyleButton extends PureComponent {
// px5: small, // px5: small,
}) })
const iconClasses = cx({
fillColorSecondary: !active,
fillColorWhite: active,
})
return ( return (
<Button <Button
className={btnClasses} className={btnClasses}
backgroundColor='none' backgroundColor='none'
onClick={this.handleOnToggle} color={color}
onClick={this.handleOnClick}
title={label} title={label}
icon={'rich-text'} icon={icon}
iconClassName={iconClasses} iconClassName={_s.inheritFill}
iconWidth='10px' iconWidth='12px'
iconHeight='10px' iconHeight='12px'
radiusSmall radiusSmall
/> />
) )
} }
} }

View File

@ -5,15 +5,18 @@ import { NavLink } from 'react-router-dom'
import { defineMessages, injectIntl } from 'react-intl' import { defineMessages, injectIntl } from 'react-intl'
import classNames from 'classnames/bind' import classNames from 'classnames/bind'
import { shortNumberFormat } from '../utils/numbers' import { shortNumberFormat } from '../utils/numbers'
import Image from './image'
import Text from './text'
import Button from './button' import Button from './button'
import DotTextSeperator from './dot_text_seperator' import DotTextSeperator from './dot_text_seperator'
import Image from './image'
import Text from './text'
const messages = defineMessages({ const messages = defineMessages({
members: { id: 'groups.card.members', defaultMessage: 'Members' }, members: { id: 'groups.card.members', defaultMessage: 'Members' },
new_statuses: { id: 'groups.sidebar-panel.item.view', defaultMessage: 'new gabs' }, new_statuses: { id: 'groups.sidebar-panel.item.view', defaultMessage: 'new gabs' },
no_recent_activity: { id: 'groups.sidebar-panel.item.no_recent_activity', defaultMessage: 'No recent activity' }, no_recent_activity: { id: 'groups.sidebar-panel.item.no_recent_activity', defaultMessage: 'No recent activity' },
viewGroup: { id: 'view_group', defaultMessage: 'View Group' },
member: { id: 'member', defaultMessage: 'Member' },
admin: { id: 'admin', defaultMessage: 'Admin' },
}) })
const mapStateToProps = (state, { id }) => ({ const mapStateToProps = (state, { id }) => ({
@ -49,8 +52,7 @@ class GroupCollectionItem extends ImmutablePureComponent {
const imageHeight = '200px' const imageHeight = '200px'
// : todo : const isMember = relationships.get('member')
const isMember = false
const outsideClasses = cx({ const outsideClasses = cx({
default: 1, default: 1,
@ -83,6 +85,25 @@ class GroupCollectionItem extends ImmutablePureComponent {
height={imageHeight} height={imageHeight}
/> />
<div className={[_s.default, _s.flexRow, _s.positionAbsolute, _s.top0, _s.right0, _s.pt10, _s.mr10].join(' ')}>
<Text
badge
className={_s.backgroundColorWhite}
size='extraSmall'
color='brand'
>
{intl.formatMessage(messages.member)}
</Text>
<Text
badge
className={[_s.backgroundColorBlack, _s.ml5].join(' ')}
size='extraSmall'
color='white'
>
{intl.formatMessage(messages.admin)}
</Text>
</div>
<div className={[_s.default, _s.px10, _s.my10].join(' ')}> <div className={[_s.default, _s.px10, _s.my10].join(' ')}>
<Text color='primary' size='medium' weight='bold'> <Text color='primary' size='medium' weight='bold'>
{group.get('title')} {group.get('title')}
@ -101,20 +122,17 @@ class GroupCollectionItem extends ImmutablePureComponent {
</div> </div>
</div> </div>
{ <div className={[_s.default, _s.px10, _s.mb10].join(' ')}>
!isMember && <Button
<div className={[_s.default, _s.px10, _s.mb10].join(' ')}> color='primary'
<Button backgroundColor='tertiary'
color='primary' radiusSmall
backgroundColor='tertiary' >
radiusSmall <Text color='inherit' weight='bold'>
> {intl.formatMessage(messages.viewGroup)}
<Text color='inherit' weight='bold'> </Text>
Join </Button>
</Text> </div>
</Button>
</div>
}
</NavLink> </NavLink>
</div> </div>

View File

@ -4,6 +4,8 @@ import AppsIcon from '../assets/apps_icon'
import AudioIcon from '../assets/audio_icon' import AudioIcon from '../assets/audio_icon'
import AudioMuteIcon from '../assets/audio_mute_icon' import AudioMuteIcon from '../assets/audio_mute_icon'
import BackIcon from '../assets/back_icon' import BackIcon from '../assets/back_icon'
import BlockquoteIcon from '../assets/blockquote_icon'
import BoldIcon from '../assets/bold_icon'
import CalendarIcon from '../assets/calendar_icon' import CalendarIcon from '../assets/calendar_icon'
import ChatIcon from '../assets/chat_icon' import ChatIcon from '../assets/chat_icon'
import CircleIcon from '../assets/circle_icon' import CircleIcon from '../assets/circle_icon'
@ -22,6 +24,7 @@ import GroupAddIcon from '../assets/group_add_icon'
import HappyIcon from '../assets/happy_icon' import HappyIcon from '../assets/happy_icon'
import HomeIcon from '../assets/home_icon' import HomeIcon from '../assets/home_icon'
import InvestorIcon from '../assets/investor_icon' import InvestorIcon from '../assets/investor_icon'
import ItalicIcon from '../assets/italic_icon'
import LikeIcon from '../assets/like_icon' import LikeIcon from '../assets/like_icon'
import LinkIcon from '../assets/link_icon' import LinkIcon from '../assets/link_icon'
import ListIcon from '../assets/list_icon' import ListIcon from '../assets/list_icon'
@ -32,6 +35,7 @@ import MinimizeFullscreenIcon from '../assets/minimize_fullscreen_icon'
import MissingIcon from '../assets/missing_icon' import MissingIcon from '../assets/missing_icon'
import MoreIcon from '../assets/more_icon' import MoreIcon from '../assets/more_icon'
import NotificationsIcon from '../assets/notifications_icon' import NotificationsIcon from '../assets/notifications_icon'
import OLListIcon from '../assets/ol_list_icon'
import PauseIcon from '../assets/pause_icon' import PauseIcon from '../assets/pause_icon'
import PinIcon from '../assets/pin_icon' import PinIcon from '../assets/pin_icon'
import PlayIcon from '../assets/play_icon' import PlayIcon from '../assets/play_icon'
@ -43,8 +47,12 @@ import SearchIcon from '../assets/search_icon'
import SearchAltIcon from '../assets/search_alt_icon' import SearchAltIcon from '../assets/search_alt_icon'
import ShareIcon from '../assets/share_icon' import ShareIcon from '../assets/share_icon'
import ShopIcon from '../assets/shop_icon' import ShopIcon from '../assets/shop_icon'
import StrikethroughIcon from '../assets/strikethrough_icon'
import SubtractIcon from '../assets/subtract_icon' import SubtractIcon from '../assets/subtract_icon'
import TextSizeIcon from '../assets/text_size_icon'
import TrendsIcon from '../assets/trends_icon' import TrendsIcon from '../assets/trends_icon'
import ULListIcon from '../assets/ul_list_icon'
import UnderlineIcon from '../assets/underline_icon'
import VerifiedIcon from '../assets/verified_icon' import VerifiedIcon from '../assets/verified_icon'
import WarningIcon from '../assets/warning_icon' import WarningIcon from '../assets/warning_icon'
@ -55,6 +63,8 @@ const ICONS = {
'audio': AudioIcon, 'audio': AudioIcon,
'audio-mute': AudioMuteIcon, 'audio-mute': AudioMuteIcon,
'back': BackIcon, 'back': BackIcon,
'blockquote': BlockquoteIcon,
'bold': BoldIcon,
'calendar': CalendarIcon, 'calendar': CalendarIcon,
'chat': ChatIcon, 'chat': ChatIcon,
'close': CloseIcon, 'close': CloseIcon,
@ -72,6 +82,7 @@ const ICONS = {
'happy': HappyIcon, 'happy': HappyIcon,
'home': HomeIcon, 'home': HomeIcon,
'investor': InvestorIcon, 'investor': InvestorIcon,
'italic': ItalicIcon,
'like': LikeIcon, 'like': LikeIcon,
'link': LinkIcon, 'link': LinkIcon,
'list': ListIcon, 'list': ListIcon,
@ -82,6 +93,7 @@ const ICONS = {
'missing': MissingIcon, 'missing': MissingIcon,
'more': MoreIcon, 'more': MoreIcon,
'notifications': NotificationsIcon, 'notifications': NotificationsIcon,
'ol-list': OLListIcon,
'pause': PauseIcon, 'pause': PauseIcon,
'pin': PinIcon, 'pin': PinIcon,
'play': PlayIcon, 'play': PlayIcon,
@ -93,8 +105,12 @@ const ICONS = {
'search-alt': SearchAltIcon, 'search-alt': SearchAltIcon,
'share': ShareIcon, 'share': ShareIcon,
'shop': ShopIcon, 'shop': ShopIcon,
'strikethrough': StrikethroughIcon,
'subtract': SubtractIcon, 'subtract': SubtractIcon,
'text-size': TextSizeIcon,
'trends': TrendsIcon, 'trends': TrendsIcon,
'ul-list': ULListIcon,
'underline': UnderlineIcon,
'verified': VerifiedIcon, 'verified': VerifiedIcon,
'warning': WarningIcon, 'warning': WarningIcon,
'': CircleIcon, '': CircleIcon,

View File

@ -1,5 +1,6 @@
import { Fragment } from 'react' import { Fragment } from 'react'
import classNames from 'classnames/bind' import classNames from 'classnames/bind'
import Button from './button'
import Icon from './icon' import Icon from './icon'
import Text from './text' import Text from './text'
@ -71,6 +72,12 @@ export default class Input extends PureComponent {
displayNone: hideLabel, displayNone: hideLabel,
}) })
const btnClasses = cx({
displayNone: value.length === 0,
px10: 1,
mr5: 1,
})
return ( return (
<Fragment> <Fragment>
{ {
@ -103,9 +110,16 @@ export default class Input extends PureComponent {
{ {
hasClear && hasClear &&
<div role='button' tabIndex='0' className={'btnClasses'} onClick={onClear}> <Button
<Icon id='close' width='10px' height='10px' className={_s.fillColorWhite} aria-label='Clear' /> className={btnClasses}
</div> tabIndex='0'
title='Clear'
onClick={onClear}
icon='close'
iconClassName={_s.inheritFill}
iconHeight='10px'
iconWidth='10px'
/>
} }
</div> </div>
</Fragment> </Fragment>

View File

@ -124,6 +124,7 @@ class Item extends ImmutablePureComponent {
let right = 'auto'; let right = 'auto';
let float = 'left'; let float = 'left';
let position = 'relative'; let position = 'relative';
let borderRadius = '0 0 0 0';
if (dimensions) { if (dimensions) {
width = dimensions.w; width = dimensions.w;
@ -134,13 +135,20 @@ class Item extends ImmutablePureComponent {
left = dimensions.l || 'auto'; left = dimensions.l || 'auto';
float = dimensions.float || 'left'; float = dimensions.float || 'left';
position = dimensions.pos || 'relative'; position = dimensions.pos || 'relative';
const br = dimensions.br || []
const hasTL = br.indexOf('tl') > -1
const hasTR = br.indexOf('tr') > -1
const hasBR = br.indexOf('br') > -1
const hasBL = br.indexOf('bl') > -1
borderRadius = `${hasTL ? '8px' : '0'} ${hasTR ? '8px' : '0'} ${hasBR ? '8px' : '0'} ${hasBL ? '8px' : '0'}`
} }
let thumbnail = ''; let thumbnail = '';
if (attachment.get('type') === 'unknown') { if (attachment.get('type') === 'unknown') {
return ( return (
<div className={[_s.default].join(' ')} key={attachment.get('id')} style={{ position, float, left, top, right, bottom, height, width: `${width}%` }}> <div className={[_s.default].join(' ')} key={attachment.get('id')} style={{ position, float, left, top, right, bottom, height, borderRadius, width: `${width}%` }}>
<a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }}> <a className='media-gallery__item-thumbnail' href={attachment.get('remote_url')} target='_blank' style={{ cursor: 'pointer' }}>
<canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' /> <canvas width={32} height={32} ref={this.setCanvasRef} className='media-gallery__preview' />
</a> </a>
@ -169,6 +177,7 @@ class Item extends ImmutablePureComponent {
href={attachment.get('remote_url') || originalUrl} href={attachment.get('remote_url') || originalUrl}
onClick={this.handleClick} onClick={this.handleClick}
target='_blank' target='_blank'
style={{ borderRadius }}
> >
<img <img
src={previewUrl} src={previewUrl}
@ -235,6 +244,7 @@ class MediaGallery extends PureComponent {
cacheWidth: PropTypes.func, cacheWidth: PropTypes.func,
visible: PropTypes.bool, visible: PropTypes.bool,
onToggleVisibility: PropTypes.func, onToggleVisibility: PropTypes.func,
reduced: PropTypes.bool,
}; };
static defaultProps = { static defaultProps = {
@ -278,8 +288,15 @@ class MediaGallery extends PureComponent {
} }
render () { render () {
const { media, intl, sensitive, height, defaultWidth } = this.props; const {
const { visible } = this.state; media,
intl,
sensitive,
height,
defaultWidth,
reduced
} = this.props
const { visible } = this.state
const width = this.state.width || defaultWidth; const width = this.state.width || defaultWidth;
@ -331,34 +348,34 @@ class MediaGallery extends PureComponent {
if (isPortrait(ar1) && isPortrait(ar2)) { if (isPortrait(ar1) && isPortrait(ar2)) {
itemsDimensions = [ itemsDimensions = [
{ w: 50, h: '100%', r: '2px' }, { w: 50, h: '100%', r: '2px', br: ['tl', 'bl'] },
{ w: 50, h: '100%', l: '2px' } { w: 50, h: '100%', l: '2px', br: ['tr', 'br'] },
]; ];
} else if (isPanoramic(ar1) && isPanoramic(ar2)) { } else if (isPanoramic(ar1) && isPanoramic(ar2)) {
itemsDimensions = [ itemsDimensions = [
{ w: 100, h: panoSize_px, b: '2px' }, { w: 100, h: panoSize_px, b: '2px', br: ['tl', 'tr'] },
{ w: 100, h: panoSize_px, t: '2px' } { w: 100, h: panoSize_px, t: '2px', br: ['bl', 'br'] },
]; ];
} else if ( } else if (
(isPanoramic(ar1) && isPortrait(ar2)) || (isPanoramic(ar1) && isPortrait(ar2)) ||
(isPanoramic(ar1) && isNonConformingRatio(ar2)) (isPanoramic(ar1) && isNonConformingRatio(ar2))
) { ) {
itemsDimensions = [ itemsDimensions = [
{ w: 100, h: `${(width / maximumAspectRatio)}px`, b: '2px' }, { w: 100, h: `${(width / maximumAspectRatio)}px`, b: '2px', br: ['tl', 'tr'] },
{ w: 100, h: `${(width * 0.6)}px`, t: '2px' }, { w: 100, h: `${(width * 0.6)}px`, t: '2px', br: ['bl', 'br'] },
]; ];
} else if ( } else if (
(isPortrait(ar1) && isPanoramic(ar2)) || (isPortrait(ar1) && isPanoramic(ar2)) ||
(isNonConformingRatio(ar1) && isPanoramic(ar2)) (isNonConformingRatio(ar1) && isPanoramic(ar2))
) { ) {
itemsDimensions = [ itemsDimensions = [
{ w: 100, h: `${(width * 0.6)}px`, b: '2px' }, { w: 100, h: `${(width * 0.6)}px`, b: '2px', br: ['tl', 'tr'] },
{ w: 100, h: `${(width / maximumAspectRatio)}px`, t: '2px' }, { w: 100, h: `${(width / maximumAspectRatio)}px`, t: '2px', br: ['bl', 'br'] },
]; ];
} else { } else {
itemsDimensions = [ itemsDimensions = [
{ w: 50, h: '100%', r: '2px' }, { w: 50, h: '100%', r: '2px', br: ['tl', 'bl'] },
{ w: 50, h: '100%', l: '2px' } { w: 50, h: '100%', l: '2px', br: ['tr', 'br'] },
]; ];
} }
} else if (size == 3) { } else if (size == 3) {
@ -374,60 +391,60 @@ class MediaGallery extends PureComponent {
if (isPanoramic(ar1) && isNonConformingRatio(ar2) && isNonConformingRatio(ar3)) { if (isPanoramic(ar1) && isNonConformingRatio(ar2) && isNonConformingRatio(ar3)) {
itemsDimensions = [ itemsDimensions = [
{ w: 100, h: `50%`, b: '2px' }, { w: 100, h: `50%`, b: '2px', br: ['tl', 'tr'] },
{ w: 50, h: '50%', t: '2px', r: '2px' }, { w: 50, h: '50%', t: '2px', r: '2px', br: ['bl'] },
{ w: 50, h: '50%', t: '2px', l: '2px' } { w: 50, h: '50%', t: '2px', l: '2px', br: ['br'] },
]; ];
} else if (isPanoramic(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) { } else if (isPanoramic(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) {
itemsDimensions = [ itemsDimensions = [
{ w: 100, h: panoSize_px, b: '4px' }, { w: 100, h: panoSize_px, b: '4px', br: ['tl', 'tr'] },
{ w: 100, h: panoSize_px }, { w: 100, h: panoSize_px },
{ w: 100, h: panoSize_px, t: '4px' } { w: 100, h: panoSize_px, t: '4px', br: ['bl', 'br'] },
]; ];
} else if (isPortrait(ar1) && isNonConformingRatio(ar2) && isNonConformingRatio(ar3)) { } else if (isPortrait(ar1) && isNonConformingRatio(ar2) && isNonConformingRatio(ar3)) {
itemsDimensions = [ itemsDimensions = [
{ w: 50, h: `100%`, r: '2px' }, { w: 50, h: `100%`, r: '2px', br: ['tl', 'bl'] },
{ w: 50, h: '50%', b: '2px', l: '2px' }, { w: 50, h: '50%', b: '2px', l: '2px', br: ['tr'] },
{ w: 50, h: '50%', t: '2px', l: '2px' }, { w: 50, h: '50%', t: '2px', l: '2px', br: ['br'] },
]; ];
} else if (isNonConformingRatio(ar1) && isNonConformingRatio(ar2) && isPortrait(ar3)) { } else if (isNonConformingRatio(ar1) && isNonConformingRatio(ar2) && isPortrait(ar3)) {
itemsDimensions = [ itemsDimensions = [
{ w: 50, h: '50%', b: '2px', r: '2px' }, { w: 50, h: '50%', b: '2px', r: '2px', br: ['tl'] },
{ w: 50, h: '50%', l: '-2px', b: '-2px', pos: 'absolute', float: 'none' }, { w: 50, h: '50%', l: '-2px', b: '-2px', pos: 'absolute', float: 'none', br: ['bl'] },
{ w: 50, h: `100%`, r: '-2px', t: '0px', b: '0px', pos: 'absolute', float: 'none' } { w: 50, h: `100%`, r: '-2px', t: '0px', b: '0px', pos: 'absolute', float: 'none', br: ['tr', 'br'] },
]; ];
} else if ( } else if (
(isNonConformingRatio(ar1) && isPortrait(ar2) && isNonConformingRatio(ar3)) || (isNonConformingRatio(ar1) && isPortrait(ar2) && isNonConformingRatio(ar3)) ||
(isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3)) (isPortrait(ar1) && isPortrait(ar2) && isPortrait(ar3))
) { ) {
itemsDimensions = [ itemsDimensions = [
{ w: 50, h: '50%', b: '2px', r: '2px' }, { w: 50, h: '50%', b: '2px', r: '2px', br: ['tl'] },
{ w: 50, h: `100%`, l: '2px', float: 'right' }, { w: 50, h: `100%`, l: '2px', float: 'right', br: ['tr', 'br'] },
{ w: 50, h: '50%', t: '2px', r: '2px' } { w: 50, h: '50%', t: '2px', r: '2px', br: ['bl'] },
]; ];
} else if ( } else if (
(isPanoramic(ar1) && isPanoramic(ar2) && isNonConformingRatio(ar3)) || (isPanoramic(ar1) && isPanoramic(ar2) && isNonConformingRatio(ar3)) ||
(isPanoramic(ar1) && isPanoramic(ar2) && isPortrait(ar3)) (isPanoramic(ar1) && isPanoramic(ar2) && isPortrait(ar3))
) { ) {
itemsDimensions = [ itemsDimensions = [
{ w: 50, h: panoSize_px, b: '2px', r: '2px' }, { w: 50, h: panoSize_px, b: '2px', r: '2px', br: ['tl'] },
{ w: 50, h: panoSize_px, b: '2px', l: '2px' }, { w: 50, h: panoSize_px, b: '2px', l: '2px', br: ['tr'] },
{ w: 100, h: `${width - panoSize}px`, t: '2px' } { w: 100, h: `${width - panoSize}px`, t: '2px', br: ['bl', 'br'] },
]; ];
} else if ( } else if (
(isNonConformingRatio(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) || (isNonConformingRatio(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) ||
(isPortrait(ar1) && isPanoramic(ar2) && isPanoramic(ar3)) (isPortrait(ar1) && isPanoramic(ar2) && isPanoramic(ar3))
) { ) {
itemsDimensions = [ itemsDimensions = [
{ w: 100, h: `${width - panoSize}px`, b: '2px' }, { w: 100, h: `${width - panoSize}px`, b: '2px', br: ['tl', 'tr'] },
{ w: 50, h: panoSize_px, t: '2px', r: '2px' }, { w: 50, h: panoSize_px, t: '2px', r: '2px', br: ['bl'] },
{ w: 50, h: panoSize_px, t: '2px', l: '2px' }, { w: 50, h: panoSize_px, t: '2px', l: '2px', br: ['br'] },
]; ];
} else { } else {
itemsDimensions = [ itemsDimensions = [
{ w: 50, h: '50%', b: '2px', r: '2px' }, { w: 50, h: '50%', b: '2px', r: '2px', br: ['tl'] },
{ w: 50, h: '50%', b: '2px', l: '2px' }, { w: 50, h: '50%', b: '2px', l: '2px', br: ['tr'] },
{ w: 100, h: `50%`, t: '2px' } { w: 100, h: `50%`, t: '2px', br: ['bl', 'br'] },
]; ];
} }
} else if (size == 4) { } else if (size == 4) {
@ -489,6 +506,12 @@ class MediaGallery extends PureComponent {
style.height = height; style.height = height;
} }
//If reduced (i.e. like in a quoted post)
//then we need to make media smaller
if (reduced) {
style.height = width / 2
}
children = media.take(4).map((attachment, i) => ( children = media.take(4).map((attachment, i) => (
<Item <Item
key={attachment.get('id')} key={attachment.get('id')}
@ -530,12 +553,15 @@ class MediaGallery extends PureComponent {
style={style} style={style}
ref={this.handleRef} ref={this.handleRef}
> >
{ /*
{ /* : todo :
<div className={classNames('spoiler-button', { 'spoiler-button--minified': visible })}> <div className={classNames('spoiler-button', { 'spoiler-button--minified': visible })}>
{spoilerButton} {spoilerButton}
</div> */ } </div> */ }
{children} <div className={[_s.default, _s.displayBlock, _s.width100PC, _s.height100PC, _s.overflowHidden].join(' ')}>
{children}
</div>
</div> </div>
); );
} }

View File

@ -0,0 +1,275 @@
import { defineMessages, injectIntl } from 'react-intl'
import {
fetchGifCategories,
fetchGifResults,
clearGifResults,
setSelectedGif,
changeGifSearchText
} from '../../actions/tenor'
import { closeModal } from '../../actions/modal'
import Block from '../block'
import Button from '../button'
import ColumnIndicator from '../column_indicator'
import Image from '../image'
import Input from '../input'
import Text from '../text'
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
title: { id: 'pick_gif', defaultMessage: 'Select a GIF' },
searchGifs: { id: 'search_gifs', defaultMessage: 'Search for GIFs' },
})
const mapStateToProps = (state) => ({
categories: state.getIn(['tenor', 'categories']),
suggestions: state.getIn(['tenor', 'suggestions']),
results: state.getIn(['tenor', 'results']),
loading: state.getIn(['tenor', 'loading']),
error: state.getIn(['tenor', 'error']),
searchText: state.getIn(['tenor', 'searchText']),
})
export const mapDispatchToProps = (dispatch) => ({
handleCloseModal() {
dispatch(changeGifSearchText(''))
dispatch(clearGifResults())
dispatch(closeModal())
},
handleFetchCategories: () => {
dispatch(fetchGifCategories())
},
handleOnChange: (value) => {
dispatch(changeGifSearchText(value))
if (value.length >= 3) {
dispatch(fetchGifResults())
} else if (value.length === 0) {
dispatch(clearGifResults())
}
},
handleSelectResult: (resultId) => {
},
// dispatchSubmit: (e) => {
// e.preventDefault();
// dispatch(getGifs());
// },
})
export default
@connect(mapStateToProps, mapDispatchToProps)
@injectIntl
class GifPickerModal extends PureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
handleCloseModal: PropTypes.func.isRequired,
handleFetchCategories: PropTypes.func.isRequired,
handleOnChange: PropTypes.func.isRequired,
categories: PropTypes.array.isRequired,
results: PropTypes.array.isRequired,
loading: PropTypes.bool,
error: PropTypes.bool,
chosenUrl: PropTypes.string,
searchText: PropTypes.string,
}
state = {
row: 0,
}
componentDidMount() {
this.props.handleFetchCategories()
}
onChange = (e) => {
this.props.handleOnChange(e.target.value)
}
onHandleCloseModal = () => {
this.props.handleCloseModal()
}
handleSelectCategory = (category) => {
this.props.handleOnChange(category)
}
handleSelectGifResult = (resultId) => {
}
render() {
const {
intl,
categories,
results,
loading,
error,
searchText
} = this.props
return (
<div style={{ width: '560px' }}>
<Block>
<div className={[_s.default, _s.flexRow, _s.alignItemsCenter, _s.justifyContentCenter, _s.borderBottom1PX, _s.borderColorSecondary, _s.height53PX, _s.px15].join(' ')}>
<div className={[_s.default, _s.flexGrow1, _s.mr5].join(' ')}>
<Input
onChange={this.onChange}
value={searchText}
prependIcon='search'
placeholder={intl.formatMessage(messages.searchGifs)}
/>
</div>
<Button
backgroundColor='none'
title={intl.formatMessage(messages.close)}
className={_s.marginLeftAuto}
onClick={this.onHandleCloseModal}
color='secondary'
icon='close'
iconWidth='10px'
iconWidth='10px'
/>
</div>
<div className={[_s.default, _s.heightMin50VH, _s.heightMax80VH, _s.overflowYScroll].join(' ')}>
{
error &&
<ColumnIndicator type='error' />
}
{
(loading && results.length === 0 && categories.length === 0) &&
<ColumnIndicator type='loading' />
}
{
(results.length > 0 || categories.length > 0) &&
<div className={[_s.default, _s.width100PC, _s.height100PC].join(' ')}>
{
results.length === 0 && categories.length > 0 &&
<GifCategoriesCollection categories={categories} handleSelectCategory={this.handleSelectCategory} />
}
{
results.length > 0 &&
<GifResultsCollection results={results} handleSelectGifResult={this.handleSelectGifResult} />
}
</div>
}
</div>
</Block>
</div>
)
}
}
class GifResultsCollectionColumn extends PureComponent {
static propTypes = {
results: PropTypes.array.isRequired,
handleSelectGifResult: PropTypes.func.isRequired,
}
onClick = (resultId) => {
this.props.handleSelectGifResult(resultId)
}
render() {
const { results } = this.props
return (
<div className={[_s.default, _s.flexNormal].join(' ')}>
{
results.map((result, i) => (
<button
key={`gif-result-item-${i}`}
onClick={() => this.onClick(result.id)}
className={[_s.default, _s.cursorPointer, _s.px2, _s.py2].join(' ')}
>
<Image
height={result.media[0].tinygif.dims[1]}
src={result.media[0].tinygif.url}
/>
</button>
))
}
</div>
)
}
}
class GifResultsCollection extends PureComponent {
static propTypes = {
results: PropTypes.array.isRequired,
handleSelectGifResult: PropTypes.func.isRequired,
}
render() {
const { results, handleSelectGifResult } = this.props
const count = results.length
const columnIndex = 10
console.log("results:", results)
return (
<div className={[_s.default, _s.height100PC, _s.flexRow, _s.width100PC].join(' ')}>
<GifResultsCollectionColumn
results={results.slice(0, columnIndex)}
handleSelectGifResult={handleSelectGifResult}
/>
<GifResultsCollectionColumn
results={results.slice(columnIndex, columnIndex * 2)}
handleSelectGifResult={handleSelectGifResult}
/>
<GifResultsCollectionColumn
results={results.slice(columnIndex * 2, count)}
handleSelectGifResult={handleSelectGifResult}
/>
</div>
)
}
}
class GifCategoriesCollection extends PureComponent {
static propTypes = {
categories: PropTypes.array.isRequired,
handleSelectCategory: PropTypes.func.isRequired,
}
onClick = (term) => {
this.props.handleSelectCategory(term)
}
render() {
const { categories } = this.props
return (
<div className={[_s.default, _s.height100PC, _s.width100PC, _s.flexRow, _s.flexWrap].join(' ')}>
{
categories.map((category, i) => (
<button
key={`gif-category-${i}`}
onClick={() => this.onClick(category.searchterm)}
className={[_s.default, _s.px2, _s.py2, _s.width50PC].join(' ')}
>
<div className={[_s.default, _s.cursorPointer].join(' ')}>
<Image
height={150}
src={category.image}
/>
<div className={[_s.default, _s.positionAbsolute, _s.videoPlayerControlsBackground, _s.right0, _s.bottom0, _s.left0, _s.py10, _s.px10].join(' ')}>
<Text color='white' weight='bold' size='large' align='left'>
{category.searchterm}
</Text>
</div>
</div>
</button>
))
}
</div>
)
}
}

View File

@ -20,6 +20,7 @@ import BoostModal from './boost_modal'
import CommunityTimelineSettingsModal from './community_timeline_settings_modal' import CommunityTimelineSettingsModal from './community_timeline_settings_modal'
import ComposeModal from './compose_modal' import ComposeModal from './compose_modal'
import ConfirmationModal from './confirmation_modal' import ConfirmationModal from './confirmation_modal'
import GifPickerModal from './gif_picker_modal'
import GroupCreateModal from './group_create_modal' import GroupCreateModal from './group_create_modal'
import GroupDeleteModal from './group_delete_modal' import GroupDeleteModal from './group_delete_modal'
import GroupEditorModal from './group_editor_modal' import GroupEditorModal from './group_editor_modal'
@ -46,6 +47,7 @@ const MODAL_COMPONENTS = {
COMPOSE: () => Promise.resolve({ default: ComposeModal }), COMPOSE: () => Promise.resolve({ default: ComposeModal }),
CONFIRM: () => Promise.resolve({ default: ConfirmationModal }), CONFIRM: () => Promise.resolve({ default: ConfirmationModal }),
EMBED: () => Promise.resolve({ default: EmbedModal }), EMBED: () => Promise.resolve({ default: EmbedModal }),
GIF_PICKER: () => Promise.resolve({ default: GifPickerModal }),
GROUP_CREATE: () => Promise.resolve({ default: GroupCreateModal }), GROUP_CREATE: () => Promise.resolve({ default: GroupCreateModal }),
GROUP_DELETE: () => Promise.resolve({ default: GroupDeleteModal }), GROUP_DELETE: () => Promise.resolve({ default: GroupDeleteModal }),
GROUP_EDITOR: () => Promise.resolve({ default: GroupEditorModal }), GROUP_EDITOR: () => Promise.resolve({ default: GroupEditorModal }),

View File

@ -1,6 +1,5 @@
import DatePicker from 'react-datepicker' import DatePicker from 'react-datepicker'
import { changeScheduledAt } from '../../actions/compose' import { changeScheduledAt } from '../../actions/compose'
import { openModal } from '../../actions/modal'
import { me } from '../../initial_state' import { me } from '../../initial_state'
import { isMobile } from '../../utils/is_mobile' import { isMobile } from '../../utils/is_mobile'
import PopoverLayout from './popover_layout' import PopoverLayout from './popover_layout'
@ -16,22 +15,15 @@ const mapDispatchToProps = dispatch => ({
setScheduledAt (date) { setScheduledAt (date) {
dispatch(changeScheduledAt(date)) dispatch(changeScheduledAt(date))
}, },
onOpenProUpgradeModal() {
dispatch(openModal('PRO_UPGRADE'))
},
}) })
export default export default
@connect(mapStateToProps, mapDispatchToProps) @connect(mapStateToProps, mapDispatchToProps)
class DatePickerPopover extends PureComponent { class DatePickerPopover extends PureComponent {
static propTypes = { static propTypes = {
date: PropTypes.instanceOf(Date), date: PropTypes.instanceOf(Date),
setScheduledAt: PropTypes.func.isRequired, setScheduledAt: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
isPro: PropTypes.bool, isPro: PropTypes.bool,
onOpenProUpgradeModal: PropTypes.func.isRequired,
position: PropTypes.string, position: PropTypes.string,
small: PropTypes.bool, small: PropTypes.bool,
} }
@ -46,7 +38,6 @@ class DatePickerPopover extends PureComponent {
const open = !!date const open = !!date
const datePickerDisabled = !isPro const datePickerDisabled = !isPro
const withPortal = isMobile(window.innerWidth) const withPortal = isMobile(window.innerWidth)
const popperPlacement = position || undefined
return ( return (
<PopoverLayout> <PopoverLayout>
@ -63,7 +54,6 @@ class DatePickerPopover extends PureComponent {
disabled={datePickerDisabled} disabled={datePickerDisabled}
showTimeSelect showTimeSelect
withPortal={withPortal} withPortal={withPortal}
popperPlacement={popperPlacement}
popperModifiers={{ popperModifiers={{
offset: { offset: {
enabled: true, enabled: true,

View File

@ -38,8 +38,7 @@ const mapStateToProps = state => ({
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
onClose(optionalType) { onClose(optionalType) {
// dispatch(closePopover(optionalType))
dispatch(closePopover())
}, },
}) })
@ -62,17 +61,6 @@ class PopoverRoot extends PureComponent {
static propTypes = { static propTypes = {
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
style: PropTypes.object,
placement: PropTypes.string,
}
static defaultProps = {
style: {},
placement: 'bottom',
}
state = {
mounted: false,
} }
handleDocumentClick = e => { handleDocumentClick = e => {
@ -85,8 +73,6 @@ class PopoverRoot extends PureComponent {
document.addEventListener('click', this.handleDocumentClick, false) document.addEventListener('click', this.handleDocumentClick, false)
document.addEventListener('keydown', this.handleKeyDown, false) document.addEventListener('keydown', this.handleKeyDown, false)
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions) document.addEventListener('touchend', this.handleDocumentClick, listenerOptions)
this.setState({ mounted: true })
} }
componentWillUnmount() { componentWillUnmount() {
@ -160,10 +146,8 @@ class PopoverRoot extends PureComponent {
render() { render() {
const { const {
type, type,
style,
props, props,
} = this.props } = this.props
const { mounted } = this.state
const visible = !!type const visible = !!type
return ( return (

View File

@ -1,5 +1,3 @@
import { Fragment } from 'react'
import { NavLink } from 'react-router-dom';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, defineMessages } from 'react-intl'; import { injectIntl, defineMessages } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
@ -9,14 +7,10 @@ import { displayMedia } from '../../initial_state';
import StatusCard from '../status_card' import StatusCard from '../status_card'
import { MediaGallery, Video } from '../../features/ui/util/async_components'; import { MediaGallery, Video } from '../../features/ui/util/async_components';
import ComposeFormContainer from '../../features/compose/containers/compose_form_container' import ComposeFormContainer from '../../features/compose/containers/compose_form_container'
import Avatar from '../avatar'; import RecursiveStatusContainer from '../../containers/recursive_status_container'
import StatusQuote from '../status_quote';
import RelativeTimestamp from '../relative_timestamp';
import DisplayName from '../display_name';
import StatusContent from '../status_content' import StatusContent from '../status_content'
import StatusPrepend from '../status_prepend'
import StatusActionBar from '../status_action_bar'; import StatusActionBar from '../status_action_bar';
import Block from '../block';
import Icon from '../icon';
import Poll from '../poll'; import Poll from '../poll';
import StatusHeader from '../status_header' import StatusHeader from '../status_header'
import Text from '../text' import Text from '../text'
@ -30,13 +24,14 @@ const cx = classNames.bind(_s)
export const textForScreenReader = (intl, status, rebloggedByText = false) => { export const textForScreenReader = (intl, status, rebloggedByText = false) => {
const displayName = status.getIn(['account', 'display_name']); const displayName = status.getIn(['account', 'display_name']);
// : todo :
const values = [ const values = [
displayName.length === 0 ? status.getIn(['account', 'acct']).split('@')[0] : displayName, // displayName.length === 0 ? status.getIn(['account', 'acct']).split('@')[0] : displayName,
status.get('spoiler_text') && status.get('hidden') // status.get('spoiler_text') && status.get('hidden')
? status.get('spoiler_text') // ? status.get('spoiler_text')
: status.get('search_index').slice(status.get('spoiler_text').length), // : status.get('search_index').slice(status.get('spoiler_text').length),
intl.formatDate(status.get('created_at'), { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }), // intl.formatDate(status.get('created_at'), { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }),
status.getIn(['account', 'acct']), // status.getIn(['account', 'acct']),
]; ];
if (rebloggedByText) { if (rebloggedByText) {
@ -47,19 +42,17 @@ export const textForScreenReader = (intl, status, rebloggedByText = false) => {
}; };
export const defaultMediaVisibility = status => { export const defaultMediaVisibility = status => {
if (!status) return undefined; if (!status) return undefined
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') { if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
status = status.get('reblog'); status = status.get('reblog')
} }
return (displayMedia !== 'hide_all' && !status.get('sensitive')) || displayMedia === 'show_all'; return (displayMedia !== 'hide_all' && !status.get('sensitive')) || displayMedia === 'show_all'
}; }
const messages = defineMessages({ const messages = defineMessages({
filtered: { id: 'status.filtered', defaultMessage: 'Filtered' }, filtered: { id: 'status.filtered', defaultMessage: 'Filtered' },
promoted: { id:'status.promoted', defaultMessage: 'Promoted gab' },
pinned: { id: 'status.pinned', defaultMessage: 'Pinned gab' },
}) })
export default export default
@ -72,7 +65,6 @@ class Status extends ImmutablePureComponent {
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map, status: ImmutablePropTypes.map,
account: ImmutablePropTypes.map,
onClick: PropTypes.func, onClick: PropTypes.func,
onReply: PropTypes.func, onReply: PropTypes.func,
onShowRevisions: PropTypes.func, onShowRevisions: PropTypes.func,
@ -91,7 +83,6 @@ class Status extends ImmutablePureComponent {
onToggleHidden: PropTypes.func, onToggleHidden: PropTypes.func,
muted: PropTypes.bool, muted: PropTypes.bool,
hidden: PropTypes.bool, hidden: PropTypes.bool,
unread: PropTypes.bool,
onMoveUp: PropTypes.func, onMoveUp: PropTypes.func,
onMoveDown: PropTypes.func, onMoveDown: PropTypes.func,
showThread: PropTypes.bool, showThread: PropTypes.bool,
@ -104,16 +95,17 @@ class Status extends ImmutablePureComponent {
onOpenProUpgradeModal: PropTypes.func, onOpenProUpgradeModal: PropTypes.func,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
borderless: PropTypes.bool, borderless: PropTypes.bool,
}; isChild: PropTypes.bool,
}
// Avoid checking props that are functions (and whose equality will always // Avoid checking props that are functions (and whose equality will always
// evaluate to false. See react-immutable-pure-component for usage. // evaluate to false. See react-immutable-pure-component for usage.
updateOnProps = ['status', 'account', 'muted', 'hidden']; updateOnProps = ['status', 'account', 'muted', 'hidden']
state = { state = {
showMedia: defaultMediaVisibility(this.props.status), showMedia: defaultMediaVisibility(this.props.status),
statusId: undefined, statusId: undefined,
}; }
// Track height changes we know about to compensate scrolling // Track height changes we know about to compensate scrolling
componentDidMount() { componentDidMount() {
@ -122,10 +114,10 @@ class Status extends ImmutablePureComponent {
getSnapshotBeforeUpdate() { getSnapshotBeforeUpdate() {
if (this.props.getScrollPosition) { if (this.props.getScrollPosition) {
return this.props.getScrollPosition(); return this.props.getScrollPosition()
} }
return null; return null
} }
static getDerivedStateFromProps(nextProps, prevState) { static getDerivedStateFromProps(nextProps, prevState) {
@ -237,34 +229,34 @@ class Status extends ImmutablePureComponent {
}; };
handleHotkeyMoveUp = e => { handleHotkeyMoveUp = e => {
this.props.onMoveUp(this.props.status.get('id'), e.target.getAttribute('data-featured')); this.props.onMoveUp(this.props.status.get('id'), e.target.getAttribute('data-featured'))
}; }
handleHotkeyMoveDown = e => { handleHotkeyMoveDown = e => {
this.props.onMoveDown(this.props.status.get('id'), e.target.getAttribute('data-featured')); this.props.onMoveDown(this.props.status.get('id'), e.target.getAttribute('data-featured'))
}; }
handleHotkeyToggleHidden = () => { handleHotkeyToggleHidden = () => {
this.props.onToggleHidden(this._properStatus()); this.props.onToggleHidden(this._properStatus())
}; }
handleHotkeyToggleSensitive = () => { handleHotkeyToggleSensitive = () => {
this.handleToggleMediaVisibility(); this.handleToggleMediaVisibility()
}; }
_properStatus() { _properStatus() {
const { status } = this.props; const { status } = this.props
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') { if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
return status.get('reblog'); return status.get('reblog')
} }
return status; return status
} }
handleRef = c => { handleRef = c => {
this.node = c; this.node = c
}; }
handleOpenProUpgradeModal = () => { handleOpenProUpgradeModal = () => {
@ -276,21 +268,30 @@ class Status extends ImmutablePureComponent {
intl, intl,
hidden, hidden,
featured, featured,
unread,
showThread, showThread,
group, group,
promoted, promoted,
borderless borderless,
isChild
} = this.props } = this.props
let media = null let media = null
let prepend, rebloggedByText, reblogContent let rebloggedByText, reblogContent
// rebloggedByText = intl.formatMessage(
// { id: 'status.reposted_by', defaultMessage: '{name} reposted' },
// { name: status.getIn(['account', 'acct']) }
// );
// reblogContent = status.get('contentHtml');
// status = status.get('reblog');
// }
let { status, ...other } = this.props;
// console.log("replies:", this.props.replies) // console.log("replies:", this.props.replies)
let { status, account, ...other } = this.props; if (!status) return null
if (status === null) return null;
if (hidden) { if (hidden) {
return ( return (
@ -302,12 +303,10 @@ class Status extends ImmutablePureComponent {
} }
if (status.get('filtered') || status.getIn(['reblog', 'filtered'])) { if (status.get('filtered') || status.getIn(['reblog', 'filtered'])) {
const minHandlers = this.props.muted const minHandlers = this.props.muted ? {} : {
? {} moveUp: this.handleHotkeyMoveUp,
: { moveDown: this.handleHotkeyMoveDown,
moveUp: this.handleHotkeyMoveUp, }
moveDown: this.handleHotkeyMoveDown,
};
return ( return (
<HotKeys handlers={minHandlers}> <HotKeys handlers={minHandlers}>
@ -315,73 +314,14 @@ class Status extends ImmutablePureComponent {
<Text>{intl.formatMessage(messages.filtered)}</Text> <Text>{intl.formatMessage(messages.filtered)}</Text>
</div> </div>
</HotKeys> </HotKeys>
); )
}
if (promoted) {
prepend = (
<button className='status__prepend status__prepend--promoted' onClick={this.handleOpenProUpgradeModal}>
<div className='status__prepend-icon-wrapper'>
<Icon id='star' className='status__prepend-icon' fixedWidth />
</div>
<Text>{intl.formatMessage(messages.promoted)}</Text>
</button>
);
} else if (featured) {
prepend = (
<div className={[_s.default, _s.flexRow, _s.alignItemsCenter, _s.borderBottom1PX, _s.borderColorSecondary, _s.py5, _s.px15].join(' ')}>
<Icon
id='pin'
width='10px'
height='10px'
className={_s.fillColorSecondary}
/>
<Text size='small' color='secondary' className={_s.ml5}>
{intl.formatMessage(messages.pinned)}
</Text>
</div>
);
} else if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
prepend = (
<div className='status__prepend'>
<div className='status__prepend-icon-wrapper'>
<Icon id='repost' className='status__prepend-icon' fixedWidth />
</div>
{/*<FormattedMessage
id='status.reposted_by'
defaultMessage='{name} reposted'
values={{
name: (
<NavLink to={`/${status.getIn(['account', 'acct'])}`} className='status__display-name muted'>
<bdi>
<strong dangerouslySetInnerHTML={display_name_html} />
</bdi>
</NavLink>
),
}}
/> */ }
</div>
);
rebloggedByText = intl.formatMessage(
{ id: 'status.reposted_by', defaultMessage: '{name} reposted' },
{ name: status.getIn(['account', 'acct']) }
);
account = status.get('account');
reblogContent = status.get('contentHtml');
status = status.get('reblog');
} }
if (status.get('poll')) { if (status.get('poll')) {
media = <Poll pollId={status.get('poll')} /> media = <Poll pollId={status.get('poll')} />
} else if (status.get('media_attachments').size > 0) { } else if (status.get('media_attachments').size > 0) {
if (status.getIn(['media_attachments', 0, 'type']) === 'video') { if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const video = status.getIn(['media_attachments', 0]); const video = status.getIn(['media_attachments', 0])
// console.log("VIDEO HERE")
media = ( media = (
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer}> <Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer}>
@ -409,6 +349,7 @@ class Status extends ImmutablePureComponent {
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}> <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
{Component => ( {Component => (
<Component <Component
reduced={isChild}
media={status.get('media_attachments')} media={status.get('media_attachments')}
sensitive={status.get('sensitive')} sensitive={status.get('sensitive')}
height={110} height={110}
@ -423,7 +364,6 @@ class Status extends ImmutablePureComponent {
) )
} }
} else if (status.get('spoiler_text').length === 0 && status.get('card')) { } else if (status.get('spoiler_text').length === 0 && status.get('card')) {
// console.log("card:", status.get('card'))
media = ( media = (
<StatusCard <StatusCard
onOpenMedia={this.props.onOpenMedia} onOpenMedia={this.props.onOpenMedia}
@ -434,10 +374,6 @@ class Status extends ImmutablePureComponent {
) )
} }
// console.log("da status:", status)
let quotedStatus = status.get('quotedStatus');
// console.log("quotedStatus:", quotedStatus)
const handlers = this.props.muted ? {} : { const handlers = this.props.muted ? {} : {
reply: this.handleHotkeyReply, reply: this.handleHotkeyReply,
favorite: this.handleHotkeyFavorite, favorite: this.handleHotkeyFavorite,
@ -451,15 +387,13 @@ class Status extends ImmutablePureComponent {
toggleSensitive: this.handleHotkeyToggleSensitive, toggleSensitive: this.handleHotkeyToggleSensitive,
} }
const statusUrl = `/${status.getIn(['account', 'acct'])}/posts/${status.get('id')}`;
const containerClasses = cx({ const containerClasses = cx({
default: 1, default: 1,
mb15: !borderless, mb15: !borderless && !isChild,
backgroundColorPrimary: 1, backgroundColorPrimary: 1,
pb15: featured, pb15: featured,
borderBottom1PX: featured, borderBottom1PX: featured && !isChild,
borderColorSecondary: featured, borderColorSecondary: featured && !isChild,
}) })
const innerContainerClasses = cx({ const innerContainerClasses = cx({
@ -468,6 +402,10 @@ class Status extends ImmutablePureComponent {
radiusSmall: !borderless, radiusSmall: !borderless,
borderColorSecondary: !borderless, borderColorSecondary: !borderless,
border1PX: !borderless, border1PX: !borderless,
pb10: isChild && status.get('media_attachments').size === 0,
pb5: isChild && status.get('media_attachments').size > 1,
cursorPointer: isChild,
backgroundSubtle_onHover: isChild,
}) })
return ( return (
@ -478,14 +416,15 @@ class Status extends ImmutablePureComponent {
data-featured={featured ? 'true' : null} data-featured={featured ? 'true' : null}
aria-label={textForScreenReader(intl, status, rebloggedByText)} aria-label={textForScreenReader(intl, status, rebloggedByText)}
ref={this.handleRef} ref={this.handleRef}
// onClick={this.handleClick}
> >
<div className={innerContainerClasses}> <div className={innerContainerClasses}>
{prepend}
<div data-id={status.get('id')}> <div data-id={status.get('id')}>
<StatusHeader status={status} /> <StatusPrepend status={status} promoted={promoted} featured={featured} />
<StatusHeader status={status} reduced={isChild} />
<div className={_s.default}> <div className={_s.default}>
<StatusContent <StatusContent
@ -500,21 +439,24 @@ class Status extends ImmutablePureComponent {
{media} {media}
{ /* status.get('quote') && <StatusQuote {
id={status.get('quote')} !!status.get('quote') && !isChild &&
/> */ } <div className={[_s.default, _s.mt10, _s.px10].join(' ')}>
<Status status={status.get('quoted_status')} isChild />
</div>
}
{
!isChild &&
<StatusActionBar status={status} {...other} />
}
{ /* showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && ( {
<button className='status__content__read-more-button' onClick={this.handleClick}> !isChild &&
<FormattedMessage id='status.show_thread' defaultMessage='Show thread' /> <div className={[_s.default, _s.borderTop1PX, _s.borderColorSecondary, _s.pt10, _s.px15, _s.mb10].join(' ')}>
</button> <ComposeFormContainer replyToId={status.get('id')} shouldCondense />
) */ } </div>
}
<StatusActionBar status={status} account={account} {...other} />
<div className={[_s.default, _s.borderTop1PX, _s.borderColorSecondary, _s.pt10, _s.px15, _s.mb10].join(' ')}>
{ /* <ComposeFormContainer replyToId={status.get('id')} shouldCondense /> */ }
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -154,7 +154,11 @@ class StatusActionBar extends ImmutablePureComponent {
) )
const hasInteractions = favoriteCount > 0 || replyCount > 0 || repostCount > 0 const hasInteractions = favoriteCount > 0 || replyCount > 0 || repostCount > 0
const shouldCondense = (!!status.get('card') || status.get('media_attachments').size > 0) && !hasInteractions const shouldCondense = (
!!status.get('card') ||
status.get('media_attachments').size > 0 ||
!!status.get('quote')
) && !hasInteractions
const containerClasses = cx({ const containerClasses = cx({
default: 1, default: 1,

View File

@ -2,6 +2,7 @@ import { Fragment } from 'react'
import { NavLink } from 'react-router-dom' import { NavLink } from 'react-router-dom'
import ImmutablePropTypes from 'react-immutable-proptypes' import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component' import ImmutablePureComponent from 'react-immutable-pure-component'
import classNames from 'classnames/bind'
import { openPopover } from '../actions/popover' import { openPopover } from '../actions/popover'
import { openModal } from '../actions/modal' import { openModal } from '../actions/modal'
import RelativeTimestamp from './relative_timestamp' import RelativeTimestamp from './relative_timestamp'
@ -12,6 +13,8 @@ import Icon from './icon'
import Button from './button' import Button from './button'
import Avatar from './avatar' import Avatar from './avatar'
const cx = classNames.bind(_s)
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
onOpenStatusRevisionsPopover(status) { onOpenStatusRevisionsPopover(status) {
dispatch(openModal('STATUS_REVISIONS', { dispatch(openModal('STATUS_REVISIONS', {
@ -36,6 +39,7 @@ class StatusHeader extends ImmutablePureComponent {
status: ImmutablePropTypes.map, status: ImmutablePropTypes.map,
onOpenStatusRevisionsPopover: PropTypes.func.isRequired, onOpenStatusRevisionsPopover: PropTypes.func.isRequired,
onOpenStatusOptionsPopover: PropTypes.func.isRequired, onOpenStatusOptionsPopover: PropTypes.func.isRequired,
reduced: PropTypes.bool,
} }
handleOpenStatusOptionsPopover = () => { handleOpenStatusOptionsPopover = () => {
@ -122,21 +126,33 @@ class StatusHeader extends ImmutablePureComponent {
} }
render() { render() {
const { status } = this.props const { status, reduced } = this.props
const statusUrl = `/${status.getIn(['account', 'acct'])}/posts/${status.get('id')}`; const statusUrl = `/${status.getIn(['account', 'acct'])}/posts/${status.get('id')}`;
const containerClasses = cx({
default: 1,
px15: 1,
py10: !reduced,
pb10: reduced,
})
const avatarSize = reduced ? 20 : 46
return ( return (
<div className={[_s.default, _s.px15, _s.py10].join(' ')}> <div className={containerClasses}>
<div className={[_s.default, _s.flexRow, _s.mt5].join(' ')}> <div className={[_s.default, _s.flexRow, _s.mt5].join(' ')}>
<NavLink {
to={`/${status.getIn(['account', 'acct'])}`} !reduced &&
title={status.getIn(['account', 'acct'])} <NavLink
className={[_s.default, _s.mr10].join(' ')} to={`/${status.getIn(['account', 'acct'])}`}
> title={status.getIn(['account', 'acct'])}
<Avatar account={status.get('account')} size={50} /> className={[_s.default, _s.mr10].join(' ')}
</NavLink> >
<Avatar account={status.get('account')} size={avatarSize} />
</NavLink>
}
<div className={[_s.default, _s.alignItemsStart, _s.flexGrow1, _s.mt5].join(' ')}> <div className={[_s.default, _s.alignItemsStart, _s.flexGrow1, _s.mt5].join(' ')}>
@ -149,18 +165,21 @@ class StatusHeader extends ImmutablePureComponent {
<DisplayName account={status.get('account')} /> <DisplayName account={status.get('account')} />
</NavLink> </NavLink>
<Button {
text !reduced &&
backgroundColor='none' <Button
color='none' text
icon='ellipsis' backgroundColor='none'
iconWidth='20px' color='none'
iconHeight='20px' icon='ellipsis'
iconClassName={_s.fillColorSecondary} iconWidth='20px'
className={_s.marginLeftAuto} iconHeight='20px'
onClick={this.handleOpenStatusOptionsPopover} iconClassName={_s.fillColorSecondary}
buttonRef={this.setStatusOptionsButton} className={_s.marginLeftAuto}
/> onClick={this.handleOpenStatusOptionsPopover}
buttonRef={this.setStatusOptionsButton}
/>
}
</div> </div>
<div className={[_s.default, _s.flexRow, _s.alignItemsCenter, _s.lineHeight15].join(' ')}> <div className={[_s.default, _s.flexRow, _s.alignItemsCenter, _s.lineHeight15].join(' ')}>

View File

@ -0,0 +1,82 @@
import { NavLink } from 'react-router-dom'
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'
import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import Icon from './icon'
import Text from './text'
const messages = defineMessages({
filtered: { id: 'status.filtered', defaultMessage: 'Filtered' },
promoted: { id: 'status.promoted', defaultMessage: 'Promoted gab' },
pinned: { id: 'status.pinned', defaultMessage: 'Pinned gab' },
reposted: { id: 'status.reposted_by', defaultMessage: '{name} reposted' },
})
export default
@injectIntl
class StatusPrepend extends ImmutablePureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
status: ImmutablePropTypes.map,
featured: PropTypes.bool,
promoted: PropTypes.bool,
}
render() {
const {
intl,
status,
featured,
promoted,
} = this.props
if (!status) return null
const isRepost = (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object')
if (!featured && !promoted && !isRepost) return null
const iconId = featured ? 'pin' : promoted ? 'star' : 'repost'
return (
<div className={[_s.default, _s.width100PC, _s.alignItemsStart, _s.borderBottom1PX, _s.borderColorSecondary].join(' ')}>
<div className={[_s.default, _s.width100PC, _s.flexRow, _s.alignItemsCenter, _s.py5, _s.px15].join(' ')}>
<Icon id={iconId} width='12px' height='12px' className={[_s.fillColorSecondary, _s.mr5].join(' ')} />
{
isRepost &&
<div className={[_s.default, _s.flexRow].join(' ')}>
<Text size='small' color='secondary'>
<FormattedMessage
id='status.reposted_by'
defaultMessage='{name} reposted'
values={{
name: (
<NavLink
className={[_s.noUnderline, _s.underline_onHover].join(' ')}
to={`/${status.getIn(['account', 'acct'])}`}
>
<Text size='small' color='secondary'>
<bdi>
<span dangerouslySetInnerHTML={{ __html: status.getIn(['account', 'display_name_html']) }} />
</bdi>
</Text>
</NavLink>
)
}}
/>
</Text>
</div>
}
{
!isRepost &&
<Text color='secondary' size='small'>
{intl.formatMessage(featured ? messages.pinned : messages.promoted)}
</Text>
}
</div>
</div>
)
}
}

View File

@ -1,40 +0,0 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import StatusContent from './status_content';
import DisplayName from './display_name';
import { NavLink } from 'react-router-dom';
const mapStateToProps = (state, { id }) => ({
status: state.getIn(['statuses', id]),
account: state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
});
@connect(mapStateToProps)
export default class StatusQuote extends PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
status: ImmutablePropTypes.map.isRequired,
};
render() {
const { status, account } = this.props;
const statusUrl = `/${account.get('acct')}/posts/${status.get('id')}`;
return (
<NavLink to={statusUrl} className="status__quote">
<DisplayName account={account} />
<StatusContent
status={status}
expanded={false}
onClick
collapsable
/>
</NavLink>
);
}
}

View File

@ -0,0 +1,64 @@
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl } from 'react-intl';
import { NavLink } from 'react-router-dom';
import Avatar from '../../../../components/avatar';
import Button from '../../../../components/button';
import DisplayName from '../../../../components/display_name';
import { isRtl } from '../../../../utils/rtl';
const messages = defineMessages({
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
});
export default
@injectIntl
class ReplyIndicator extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
status: ImmutablePropTypes.map,
onCancel: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
handleClick = () => {
this.props.onCancel();
}
render () {
const { status, intl } = this.props;
if (!status) {
return null;
}
const content = { __html: status.get('contentHtml') };
const style = {
direction: isRtl(status.get('search_index')) ? 'rtl' : 'ltr',
};
return (
<div className='reply-indicator'>
<div className='reply-indicator__header'>
<div className='reply-indicator__cancel'>
<Button title={intl.formatMessage(messages.cancel)} icon='times' onClick={this.handleClick} inverted />
</div>
<NavLink to={`/${status.getIn(['account', 'acct'])}`} className='reply-indicator__display-name'>
<div className='reply-indicator__display-avatar'>
<Avatar account={status.get('account')} size={24} />
</div>
<DisplayName account={status.get('account')} />
</NavLink>
</div>
<div className='reply-indicator-content' style={style} dangerouslySetInnerHTML={content} />
</div>
);
}
}

View File

@ -43,6 +43,7 @@ export default class Text extends PureComponent {
weight: PropTypes.oneOf(Object.keys(WEIGHTS)), weight: PropTypes.oneOf(Object.keys(WEIGHTS)),
align: PropTypes.oneOf(Object.keys(ALIGNMENTS)), align: PropTypes.oneOf(Object.keys(ALIGNMENTS)),
underline: PropTypes.bool, underline: PropTypes.bool,
badge: PropTypes.bool,
htmlFor: PropTypes.string, htmlFor: PropTypes.string,
} }
@ -63,13 +64,18 @@ export default class Text extends PureComponent {
weight, weight,
underline, underline,
align, align,
htmlFor htmlFor,
badge
} = this.props } = this.props
const classes = cx(className, { const classes = cx(className, {
default: 1, default: 1,
text: 1, text: 1,
radiusSmall: badge,
lineHeight15: badge,
px5: badge,
colorPrimary: color === COLORS.primary, colorPrimary: color === COLORS.primary,
colorSecondary: color === COLORS.secondary, colorSecondary: color === COLORS.secondary,
colorBrand: color === COLORS.brand, colorBrand: color === COLORS.brand,

View File

@ -63,9 +63,7 @@ class TimelineComposeBlock extends ImmutablePureComponent {
{intl.formatMessage(messages.createPost)} {intl.formatMessage(messages.createPost)}
</Heading> </Heading>
</div> </div>
<div className={[_s.default, _s.flexRow, _s.px15, _s.pt15, _s.pb10].join(' ')}> <ComposeFormContainer {...rest} />
<ComposeFormContainer {...rest} />
</div>
</Block> </Block>
</section> </section>
) )

View File

@ -488,7 +488,6 @@ class Video extends PureComponent {
return ( return (
<div <div
role='menuitem'
className={[_s.default].join(' ')} className={[_s.default].join(' ')}
style={playerStyle} style={playerStyle}
ref={this.setPlayerRef} ref={this.setPlayerRef}

View File

@ -0,0 +1,9 @@
// import StatusContainer from './status_container'
// export default class RecursiveStatusContainer extends PureComponent {
// render() {
// return (
// <StatusContainer id={this.props.id} />
// )
// }
// }

View File

@ -27,7 +27,7 @@ export default class ComposeExtraButton extends PureComponent {
} = this.props } = this.props
const btnClasses = cx({ const btnClasses = cx({
backgroundSubtle_onHover: 1, backgroundSubtle_onHover: !active,
backgroundColorBrandLight: active, backgroundColorBrandLight: active,
py10: !small, py10: !small,
px10: !small, px10: !small,

View File

@ -16,7 +16,6 @@ import EmojiPickerButton from '../../components/emoji_picker_button'
import PollFormContainer from '../../containers/poll_form_container' import PollFormContainer from '../../containers/poll_form_container'
import SchedulePostButton from '../schedule_post_button' import SchedulePostButton from '../schedule_post_button'
import QuotedStatusPreviewContainer from '../../containers/quoted_status_preview_container' import QuotedStatusPreviewContainer from '../../containers/quoted_status_preview_container'
import Icon from '../../../../components/icon'
import Button from '../../../../components/button' import Button from '../../../../components/button'
import Avatar from '../../../../components/avatar' import Avatar from '../../../../components/avatar'
import { isMobile } from '../../../../utils/is_mobile' import { isMobile } from '../../../../utils/is_mobile'
@ -238,7 +237,13 @@ class ComposeForm extends ImmutablePureComponent {
const disabledButton = disabled || this.props.isUploading || this.props.isChangingUpload || length(text) > maxPostCharacterCount || (text.length !== 0 && text.trim().length === 0 && !anyMedia); const disabledButton = disabled || this.props.isUploading || this.props.isChangingUpload || length(text) > maxPostCharacterCount || (text.length !== 0 && text.trim().length === 0 && !anyMedia);
const shouldAutoFocus = autoFocus && !showSearch && !isMobile(window.innerWidth) const shouldAutoFocus = autoFocus && !showSearch && !isMobile(window.innerWidth)
const containerClasses = cx({ const parentContainerClasses = cx({
default: 1,
flexRow: !shouldCondense,
pb10: !shouldCondense,
})
const childContainerClasses = cx({
default: 1, default: 1,
flexNormal: 1, flexNormal: 1,
flexRow: shouldCondense, flexRow: shouldCondense,
@ -252,111 +257,114 @@ class ComposeForm extends ImmutablePureComponent {
flexRow: 1, flexRow: 1,
alignItemsCenter: 1, alignItemsCenter: 1,
mt10: !shouldCondense, mt10: !shouldCondense,
px15: !shouldCondense,
}) })
const contentWarningClasses = cx({ const contentWarningClasses = cx({
default: 1, default: 1,
pt5: 1, px15: 1,
pb10: 1, py10: 1,
borderBottom1PX: 1, borderBottom1PX: 1,
borderColorSecondary: 1, borderColorSecondary: 1,
mb10: 1,
displayNone: !spoiler displayNone: !spoiler
}) })
return ( return (
<div className={[_s.default, _s.flexRow, _s.width100PC].join(' ')}> <div className={parentContainerClasses}>
{ <div className={[_s.default, _s.flexRow, _s.width100PC].join(' ')}>
shouldCondense && {
<div className={[_s.default, _s.mr10, _s.mt5]}> shouldCondense &&
<Avatar account={account} size='28' /> <div className={[_s.default, _s.mr10, _s.mt5].join(' ')}>
</div> <Avatar account={account} size='28' />
} </div>
}
<div <div
className={containerClasses} className={childContainerClasses}
ref={this.setForm} ref={this.setForm}
onClick={this.handleClick} onClick={this.handleClick}
>
<div className={contentWarningClasses}>
<AutosuggestTextbox
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
value={this.props.spoilerText}
onChange={this.handleChangeSpoilerText}
onKeyDown={this.handleKeyDown}
disabled={!this.props.spoiler}
ref={this.setSpoilerText}
suggestions={this.props.suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSpoilerSuggestionSelected}
searchTokens={[':']}
id='cw-spoiler-input'
/>
</div>
<AutosuggestTextbox
ref={(isModalOpen && shouldCondense) ? null : this.setAutosuggestTextarea}
placeholder={intl.formatMessage(messages.placeholder)}
disabled={disabled}
value={this.props.text}
onChange={this.handleChange}
suggestions={this.props.suggestions}
onKeyDown={this.handleKeyDown}
onFocus={this.handleComposeFocus}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}
onPaste={onPaste}
autoFocus={shouldAutoFocus}
small={shouldCondense}
textarea
> >
<div className='compose-form__modifiers'> <div className={contentWarningClasses}>
<UploadForm replyToId={replyToId} /> <AutosuggestTextbox
{ placeholder={intl.formatMessage(messages.spoiler_placeholder)}
!edit && value={this.props.spoilerText}
<PollFormContainer replyToId={replyToId} /> onChange={this.handleChangeSpoilerText}
} onKeyDown={this.handleKeyDown}
disabled={!this.props.spoiler}
ref={this.setSpoilerText}
suggestions={this.props.suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSpoilerSuggestionSelected}
searchTokens={[':']}
prependIcon='warning'
id='cw-spoiler-input'
/>
</div> </div>
</AutosuggestTextbox> <AutosuggestTextbox
ref={(isModalOpen && shouldCondense) ? null : this.setAutosuggestTextarea}
placeholder={intl.formatMessage(messages.placeholder)}
disabled={disabled}
value={this.props.text}
onChange={this.handleChange}
suggestions={this.props.suggestions}
onKeyDown={this.handleKeyDown}
onFocus={this.handleComposeFocus}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}
onPaste={onPaste}
autoFocus={shouldAutoFocus}
small={shouldCondense}
textarea
>
{ /* quoteOfId && <QuotedStatusPreviewContainer id={quoteOfId} /> */} <div className={[_s.default, _s.px15].join(' ')}>
<UploadForm replyToId={replyToId} />
{
!edit &&
<PollFormContainer replyToId={replyToId} />
}
</div>
<div className={actionsContainerClasses}> </AutosuggestTextbox>
<div className={[_s.default, _s.flexRow, _s.marginRightAuto].join(' ')}>
<RichTextEditorButton small={shouldCondense} /> { /* quoteOfId && <QuotedStatusPreviewContainer id={quoteOfId} /> */}
<UploadButton small={shouldCondense} />
{ <div className={actionsContainerClasses}>
!edit && <PollButton small={shouldCondense} /> <div className={[_s.default, _s.flexRow, _s.marginRightAuto].join(' ')}>
} <RichTextEditorButton small={shouldCondense} />
<UploadButton small={shouldCondense} />
{
!edit && <PollButton small={shouldCondense} />
}
{
!shouldCondense &&
<StatusVisibilityButton small={shouldCondense} />
}
<SpoilerButton small={shouldCondense} />
<SchedulePostButton small={shouldCondense} />
<GifSelectorButton small={shouldCondense} />
<EmojiPickerButton small={shouldCondense} />
</div>
<CharacterCounter max={maxPostCharacterCount} text={text} small={shouldCondense} />
{ {
!shouldCondense && !shouldCondense &&
<StatusVisibilityButton small={shouldCondense} /> <Button
className={[_s.fontSize15PX, _s.px15].join(' ')}
onClick={this.handleSubmit}
disabled={disabledButton}
>
{intl.formatMessage(scheduledAt ? messages.schedulePost : messages.publish)}
</Button>
} }
<SpoilerButton small={shouldCondense} />
<SchedulePostButton small={shouldCondense} />
<GifSelectorButton small={shouldCondense} />
<EmojiPickerButton small={shouldCondense} />
</div> </div>
<CharacterCounter max={maxPostCharacterCount} text={text} small={shouldCondense} />
{
!shouldCondense &&
<Button
className={[_s.fontSize15PX, _s.px15].join(' ')}
onClick={this.handleSubmit}
disabled={disabledButton}
>
{intl.formatMessage(scheduledAt ? messages.schedulePost : messages.publish)}
</Button>
}
</div> </div>
</div> </div>
</div> </div>
) )

View File

@ -7,8 +7,7 @@ const messages = defineMessages({
}) })
const mapStateToProps = state => ({ const mapStateToProps = state => ({
// unavailable: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 0), active: state.get('popover').popoverType === 'EMOJI_PICKER',
// active: state.getIn(['compose', 'poll']) !== null,
}) })
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
@ -33,7 +32,8 @@ class EmojiPickerButton extends PureComponent {
small: PropTypes.bool, small: PropTypes.bool,
} }
handleClick = () => { handleClick = (e) => {
e.preventDefault()
this.props.onClick(this.button) this.props.onClick(this.button)
} }
@ -44,13 +44,11 @@ class EmojiPickerButton extends PureComponent {
render() { render() {
const { active, small, intl } = this.props const { active, small, intl } = this.props
const title = intl.formatMessage(messages.emoji)
return ( return (
<ComposeExtraButton <ComposeExtraButton
title={intl.formatMessage(messages.emoji)}
onClick={this.handleClick} onClick={this.handleClick}
icon='happy' icon='happy'
title={title}
small={small} small={small}
active={active} active={active}
buttonRef={this.setButton} buttonRef={this.setButton}

View File

@ -1,6 +1,6 @@
import { injectIntl, defineMessages } from 'react-intl' import { injectIntl, defineMessages } from 'react-intl'
import { changeComposeSpoilerness } from '../../../actions/compose'
import ComposeExtraButton from './compose_extra_button' import ComposeExtraButton from './compose_extra_button'
import { openModal } from '../../../actions/modal'
const messages = defineMessages({ const messages = defineMessages({
marked: { id: 'compose_form.spoiler.marked', defaultMessage: 'Text is hidden behind warning' }, marked: { id: 'compose_form.spoiler.marked', defaultMessage: 'Text is hidden behind warning' },
@ -9,13 +9,13 @@ const messages = defineMessages({
}) })
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
active: state.getIn(['compose', 'spoiler']), active: !!state.getIn(['compose', 'gif']) || state.get('modal').modalType === 'GIF_PICKER',
}) })
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = (dispatch) => ({
onClick () { onClick() {
dispatch(changeComposeSpoilerness()) dispatch(openModal('GIF_PICKER'))
}, },
}) })
@ -23,29 +23,35 @@ const mapDispatchToProps = dispatch => ({
export default export default
@injectIntl @injectIntl
@connect(mapStateToProps, mapDispatchToProps) @connect(mapStateToProps, mapDispatchToProps)
class SpoilerButton extends PureComponent { class GifSelectorButton extends PureComponent {
static propTypes = { static propTypes = {
active: PropTypes.bool,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
onClick: PropTypes.func.isRequired,
active: PropTypes.bool,
small: PropTypes.bool, small: PropTypes.bool,
} }
handleClick = (e) => { handleClick = (e) => {
e.preventDefault() e.preventDefault()
this.props.onClick() this.props.onClick(this.button)
} }
render () { setButton = (n) => {
const { active, intl, small } = this.props this.button = n
}
render() {
const { active, small, intl } = this.props
return ( return (
<ComposeExtraButton <ComposeExtraButton
title={intl.formatMessage(messages.title)} title={intl.formatMessage(messages.title)}
icon='gif'
onClick={this.handleClick} onClick={this.handleClick}
icon='gif'
small={small} small={small}
active={active} active={active}
buttonRef={this.setButton}
/> />
) )
} }

View File

@ -1,5 +1,5 @@
import { injectIntl, defineMessages } from 'react-intl' import { injectIntl, defineMessages } from 'react-intl'
import { changeComposeSpoilerness } from '../../../actions/compose' import { changeRichTextEditorControlsVisibility } from '../../../actions/compose'
import ComposeExtraButton from './compose_extra_button' import ComposeExtraButton from './compose_extra_button'
const messages = defineMessages({ const messages = defineMessages({
@ -9,13 +9,13 @@ const messages = defineMessages({
}) })
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
active: state.getIn(['compose', 'spoiler']), active: state.getIn(['compose', 'rte_controls_visible']),
}) })
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
onClick () { onClick (status) {
dispatch(changeComposeSpoilerness()) dispatch(changeRichTextEditorControlsVisibility(status))
}, },
}) })
@ -23,7 +23,7 @@ const mapDispatchToProps = dispatch => ({
export default export default
@injectIntl @injectIntl
@connect(mapStateToProps, mapDispatchToProps) @connect(mapStateToProps, mapDispatchToProps)
class SpoilerButton extends PureComponent { class RichTextEditorButton extends PureComponent {
static propTypes = { static propTypes = {
active: PropTypes.bool, active: PropTypes.bool,

View File

@ -1,6 +1,7 @@
import ImmutablePropTypes from 'react-immutable-proptypes' import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component' import ImmutablePureComponent from 'react-immutable-pure-component'
import { fetchGroups } from '../actions/groups' import { fetchGroups } from '../actions/groups'
import ColumnIndicator from '../components/column_indicator'
import ScrollableList from '../components/scrollable_list' import ScrollableList from '../components/scrollable_list'
import GroupCollectionItem from '../components/group_collection_item' import GroupCollectionItem from '../components/group_collection_item'
@ -30,6 +31,9 @@ class GroupsCollection extends ImmutablePureComponent {
render() { render() {
const { groupIds } = this.props const { groupIds } = this.props
if (!groupIds) {
return <ColumnIndicator type='loading' />
}
return ( return (
<div className={[_s.default, _s.flexRow, _s.flexWrap].join(' ')}> <div className={[_s.default, _s.flexRow, _s.flexWrap].join(' ')}>

View File

@ -27,5 +27,6 @@ export const promotions = initialState && initialState.promotions;
export const unreadCount = getMeta('unread_count'); export const unreadCount = getMeta('unread_count');
export const monthlyExpensesComplete = getMeta('monthly_expenses_complete'); export const monthlyExpensesComplete = getMeta('monthly_expenses_complete');
export const favouritesCount = getMeta('favourites_count'); export const favouritesCount = getMeta('favourites_count');
export const tenorkey = getMeta('tenorkey');
export default initialState; export default initialState;

View File

@ -1,10 +1,18 @@
import { Fragment } from 'react' import { Fragment } from 'react'
import { me } from '../initial_state'
import { openModal } from '../actions/modal' import { openModal } from '../actions/modal'
import LinkFooter from '../components/link_footer' import LinkFooter from '../components/link_footer'
import GroupsPanel from '../components/panel/groups_panel' import GroupsPanel from '../components/panel/groups_panel'
import WhoToFollowPanel from '../components/panel/who_to_follow_panel' import WhoToFollowPanel from '../components/panel/who_to_follow_panel'
import DefaultLayout from '../layouts/default_layout' import DefaultLayout from '../layouts/default_layout'
const mapStateToProps = (state) => {
const account = state.getIn(['accounts', me])
return {
isPro: account.get('is_pro'),
}
}
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
onOpenGroupCreateModal() { onOpenGroupCreateModal() {
@ -13,10 +21,11 @@ const mapDispatchToProps = dispatch => ({
}) })
export default export default
@connect(null, mapDispatchToProps) @connect(mapStateToProps, mapDispatchToProps)
class GroupsPage extends PureComponent { class GroupsPage extends PureComponent {
static propTypes = { static propTypes = {
isPro: PropTypes.bool,
onOpenGroupCreateModal: PropTypes.func.isRequired, onOpenGroupCreateModal: PropTypes.func.isRequired,
} }
@ -25,9 +34,9 @@ class GroupsPage extends PureComponent {
} }
render() { render() {
const { children, onOpenGroupCreateModal } = this.props const { children, isPro, onOpenGroupCreateModal } = this.props
const tabs = [ let tabs = [
{ {
title: 'Featured', title: 'Featured',
to: '/groups' to: '/groups'
@ -40,21 +49,25 @@ class GroupsPage extends PureComponent {
title: 'My Groups', title: 'My Groups',
to: '/groups/browse/member' to: '/groups/browse/member'
}, },
{ // only if is_pro ]
let actions = []
if (isPro) {
actions = [{
icon: 'group-add',
onClick: onOpenGroupCreateModal,
}]
tabs.push({
title: 'Admin', title: 'Admin',
to: '/groups/browse/admin' to: '/groups/browse/admin'
}, })
] }
return ( return (
<DefaultLayout <DefaultLayout
title='Groups' title='Groups'
actions={[ actions={actions}
{
icon: 'group-add',
onClick: onOpenGroupCreateModal
},
]}
layout={( layout={(
<Fragment> <Fragment>
<WhoToFollowPanel /> <WhoToFollowPanel />

View File

@ -54,6 +54,7 @@ const initialState = ImmutableMap({
spoiler_text: '', spoiler_text: '',
privacy: null, privacy: null,
text: '', text: '',
markdown_text: '',
focusDate: null, focusDate: null,
caretPosition: null, caretPosition: null,
preselectDate: null, preselectDate: null,
@ -75,6 +76,7 @@ const initialState = ImmutableMap({
tagHistory: ImmutableList(), tagHistory: ImmutableList(),
scheduled_at: null, scheduled_at: null,
rte_controls_visible: false, rte_controls_visible: false,
gif: null,
}); });
const initialPoll = ImmutableMap({ const initialPoll = ImmutableMap({
@ -298,6 +300,7 @@ export default function compose(state = initialState, action) {
map.set('poll', null); map.set('poll', null);
map.set('idempotencyKey', uuid()); map.set('idempotencyKey', uuid());
map.set('scheduled_at', null); map.set('scheduled_at', null);
map.set('rte_controls_visible', false);
}); });
case COMPOSE_SUBMIT_REQUEST: case COMPOSE_SUBMIT_REQUEST:
return state.set('is_submitting', true); return state.set('is_submitting', true);
@ -389,7 +392,9 @@ export default function compose(state = initialState, action) {
case COMPOSE_SCHEDULED_AT_CHANGE: case COMPOSE_SCHEDULED_AT_CHANGE:
return state.set('scheduled_at', action.date); return state.set('scheduled_at', action.date);
case COMPOSE_RICH_TEXT_EDITOR_CONTROLS_VISIBILITY: case COMPOSE_RICH_TEXT_EDITOR_CONTROLS_VISIBILITY:
return '' return state.withMutations(map => {
map.set('rte_controls_visible', !state.get('rte_controls_visible'));
});
default: default:
return state; return state;
} }

View File

@ -0,0 +1,55 @@
import {
GIFS_CLEAR_RESULTS,
GIF_SET_SELECTED,
GIF_CHANGE_SEARCH_TEXT,
GIF_RESULTS_FETCH_REQUEST,
GIF_RESULTS_FETCH_SUCCESS,
GIF_RESULTS_FETCH_FAIL,
GIF_CATEGORIES_FETCH_REQUEST,
GIF_CATEGORIES_FETCH_SUCCESS,
GIF_CATEGORIES_FETCH_FAIL
} from '../actions/tenor'
import { Map as ImmutableMap } from 'immutable'
const initialState = ImmutableMap({
categories: [],
results: [],
chosenUrl: '',
searchText: '',
loading: false,
error: false,
})
export default function (state = initialState, action) {
switch (action.type) {
case GIF_RESULTS_FETCH_REQUEST:
case GIF_CATEGORIES_FETCH_REQUEST:
return state.set('loading', true)
case GIF_RESULTS_FETCH_SUCCESS:
return state.withMutations(map => {
map.set('results', action.results);
map.set('error', false);
map.set('loading', false);
});
case GIF_CATEGORIES_FETCH_SUCCESS:
return state.withMutations(map => {
map.set('categories', action.categories);
map.set('error', false);
map.set('loading', false);
});
case GIF_RESULTS_FETCH_FAIL:
case GIF_CATEGORIES_FETCH_FAIL:
return state.withMutations(map => {
map.set('error', !!action.error);
map.set('loading', false);
});
case GIFS_CLEAR_RESULTS:
return state.set('results', [])
case GIF_SET_SELECTED:
return state.set('chosenUrl', action.url)
case GIF_CHANGE_SEARCH_TEXT:
return state.set('searchText', action.text.trim());
default:
return state
}
}

View File

@ -2,32 +2,32 @@ import {
GROUP_FETCH_SUCCESS, GROUP_FETCH_SUCCESS,
GROUP_FETCH_FAIL, GROUP_FETCH_FAIL,
GROUPS_FETCH_SUCCESS, GROUPS_FETCH_SUCCESS,
} from '../actions/groups'; } from '../actions/groups'
import { GROUP_UPDATE_SUCCESS } from '../actions/group_editor'; import { GROUP_UPDATE_SUCCESS } from '../actions/group_editor'
import { Map as ImmutableMap, fromJS } from 'immutable'; import { Map as ImmutableMap, fromJS } from 'immutable'
const initialState = ImmutableMap(); const initialState = ImmutableMap()
const normalizeGroup = (state, group) => state.set(group.id, fromJS(group)); const normalizeGroup = (state, group) => state.set(group.id, fromJS(group))
const normalizeGroups = (state, groups) => { const normalizeGroups = (state, groups) => {
groups.forEach(group => { groups.forEach(group => {
state = normalizeGroup(state, group); state = normalizeGroup(state, group)
}); })
return state; return state
}; }
export default function groups(state = initialState, action) { export default function groups(state = initialState, action) {
switch(action.type) { switch(action.type) {
case GROUP_FETCH_SUCCESS: case GROUP_FETCH_SUCCESS:
case GROUP_UPDATE_SUCCESS: case GROUP_UPDATE_SUCCESS:
return normalizeGroup(state, action.group); return normalizeGroup(state, action.group)
case GROUPS_FETCH_SUCCESS: case GROUPS_FETCH_SUCCESS:
return normalizeGroups(state, action.groups); return normalizeGroups(state, action.groups)
case GROUP_FETCH_FAIL: case GROUP_FETCH_FAIL:
return state.set(action.id, false); return state.set(action.id, false)
default: default:
return state; return state
} }
}; }

View File

@ -1,82 +1,84 @@
import { combineReducers } from 'redux-immutable' import { combineReducers } from 'redux-immutable'
import popover from './popover'
import timelines from './timelines'
import meta from './meta'
import { loadingBarReducer } from 'react-redux-loading-bar' import { loadingBarReducer } from 'react-redux-loading-bar'
import modal from './modal'
import user_lists from './user_lists'
import domain_lists from './domain_lists'
import accounts from './accounts' import accounts from './accounts'
import accounts_counters from './accounts_counters' import accounts_counters from './accounts_counters'
import statuses from './statuses'
import relationships from './relationships'
import settings from './settings'
import push_notifications from './push_notifications'
import status_lists from './status_lists'
import mutes from './mutes'
import reports from './reports'
import contexts from './contexts'
import compose from './compose' import compose from './compose'
import search from './search' import contexts from './contexts'
import media_attachments from './media_attachments'
import notifications from './notifications'
import height_cache from './height_cache'
import custom_emojis from './custom_emojis'
import lists from './lists'
import listEditor from './list_editor'
import listAdder from './list_adder'
import filters from './filters'
import conversations from './conversations' import conversations from './conversations'
import suggestions from './suggestions' import custom_emojis from './custom_emojis'
import polls from './polls' import domain_lists from './domain_lists'
import identity_proofs from './identity_proofs' import filters from './filters'
import hashtags from './hashtags'
import groups from './groups' import groups from './groups'
import group_relationships from './group_relationships'
import group_lists from './group_lists'
import group_editor from './group_editor' import group_editor from './group_editor'
import group_lists from './group_lists'
import group_relationships from './group_relationships'
import hashtags from './hashtags'
import height_cache from './height_cache'
import identity_proofs from './identity_proofs'
import lists from './lists'
import listAdder from './list_adder'
import listEditor from './list_editor'
import media_attachments from './media_attachments'
import meta from './meta'
import modal from './modal'
import mutes from './mutes'
import notifications from './notifications'
import polls from './polls'
import popover from './popover'
import push_notifications from './push_notifications'
import relationships from './relationships'
import reports from './reports'
import search from './search'
import settings from './settings'
import sidebar from './sidebar' import sidebar from './sidebar'
import statuses from './statuses'
import status_lists from './status_lists'
import status_revisions from './status_revisions' import status_revisions from './status_revisions'
import suggestions from './suggestions'
import tenor from './tenor'
import timelines from './timelines'
import user_lists from './user_lists'
const reducers = { const reducers = {
popover,
timelines,
meta,
loadingBar: loadingBarReducer,
modal,
user_lists,
domain_lists,
status_lists,
accounts, accounts,
accounts_counters, accounts_counters,
statuses,
relationships,
settings,
push_notifications,
mutes,
reports,
contexts,
compose, compose,
search, contexts,
media_attachments, conversations,
notifications,
height_cache,
custom_emojis, custom_emojis,
domain_lists,
filters,
groups,
group_editor,
group_lists,
group_relationships,
hashtags,
height_cache,
identity_proofs, identity_proofs,
lists, lists,
listEditor,
listAdder, listAdder,
filters, listEditor,
conversations, loadingBar: loadingBarReducer,
suggestions, media_attachments,
meta,
modal,
mutes,
notifications,
polls, polls,
hashtags, popover,
groups, push_notifications,
group_relationships, relationships,
group_lists, reports,
group_editor, search,
settings,
sidebar, sidebar,
statuses,
status_lists,
status_revisions, status_revisions,
suggestions,
tenor,
timelines,
user_lists,
} }
export default combineReducers(reducers) export default combineReducers(reducers)

View File

@ -0,0 +1,55 @@
import {
GIFS_CLEAR_RESULTS,
GIF_SET_SELECTED,
GIF_CHANGE_SEARCH_TEXT,
GIF_RESULTS_FETCH_REQUEST,
GIF_RESULTS_FETCH_SUCCESS,
GIF_RESULTS_FETCH_FAIL,
GIF_CATEGORIES_FETCH_REQUEST,
GIF_CATEGORIES_FETCH_SUCCESS,
GIF_CATEGORIES_FETCH_FAIL
} from '../actions/tenor'
import { Map as ImmutableMap } from 'immutable'
const initialState = ImmutableMap({
categories: [],
results: [],
chosenUrl: '',
searchText: '',
loading: false,
error: false,
})
export default function (state = initialState, action) {
switch (action.type) {
case GIF_RESULTS_FETCH_REQUEST:
case GIF_CATEGORIES_FETCH_REQUEST:
return state.set('loading', true)
case GIF_RESULTS_FETCH_SUCCESS:
return state.withMutations(map => {
map.set('results', action.results);
map.set('error', false);
map.set('loading', false);
});
case GIF_CATEGORIES_FETCH_SUCCESS:
return state.withMutations(map => {
map.set('categories', action.categories);
map.set('error', false);
map.set('loading', false);
});
case GIF_RESULTS_FETCH_FAIL:
case GIF_CATEGORIES_FETCH_FAIL:
return state.withMutations(map => {
map.set('error', !!action.error);
map.set('loading', false);
});
case GIFS_CLEAR_RESULTS:
return state.set('results', [])
case GIF_SET_SELECTED:
return state.set('chosenUrl', action.url)
case GIF_CHANGE_SEARCH_TEXT:
return state.set('searchText', action.text.trim());
default:
return state
}
}

View File

@ -70,12 +70,13 @@ export const makeGetStatus = () => {
(state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'quote_of_id'])]), (state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'quote_of_id'])]),
(state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]), (state, { id }) => state.getIn(['statuses', state.getIn(['statuses', id, 'reblog'])]),
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]), (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'quote_of_id']), 'account'])]),
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]), (state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
(state, { username }) => username, (state, { username }) => username,
getFilters, getFilters,
], ],
(statusBase, quotedStatus, statusRepost, accountBase, accountRepost, username, filters) => { (statusBase, quotedStatus, statusRepost, accountBase, accountQuoted, accountRepost, username, filters) => {
if (!statusBase) { if (!statusBase) {
return null; return null;
} }
@ -92,6 +93,10 @@ export const makeGetStatus = () => {
statusRepost = null; statusRepost = null;
} }
if (quotedStatus) {
quotedStatus = quotedStatus.set('account', accountQuoted);
}
const regex = (accountRepost || accountBase).get('id') !== me && regexFromFilters(filters); const regex = (accountRepost || accountBase).get('id') !== me && regexFromFilters(filters);
const filtered = regex && regex.test(statusBase.get('reblog') ? statusRepost.get('search_index') : statusBase.get('search_index')); const filtered = regex && regex.test(statusBase.get('reblog') ? statusRepost.get('search_index') : statusBase.get('search_index'));

View File

@ -248,6 +248,10 @@ body {
display: inline-block; display: inline-block;
} }
.cursorText {
cursor: text;
}
.cursorPointer { .cursorPointer {
cursor: pointer cursor: pointer
} }
@ -472,6 +476,10 @@ body {
min-height: 50vh; min-height: 50vh;
} }
.heightMin50PX {
min-height: 50px;
}
.height100VH { .height100VH {
height: 100vh; height: 100vh;
} }
@ -935,6 +943,11 @@ body {
padding-bottom: 2px; padding-bottom: 2px;
} }
.px2 {
padding-left: 2px;
padding-right: 2px;
}
.pb15 { .pb15 {
padding-bottom: 15px; padding-bottom: 15px;
} }
@ -1016,4 +1029,27 @@ body {
.visibilityHidden { .visibilityHidden {
visibility: hidden; visibility: hidden;
}
/**
* Rich Text Editor
*/
.RTE :global(.RichEditor-hidePlaceholder .public-DraftEditorPlaceholder-root) {
display: none;
}
.RTE :global(.RichEditor-editor .RichEditor-blockquote) {
border-left: 5px solid #eee;
color: #666;
font-family: 'Hoefler Text', 'Georgia', serif;
font-style: italic;
margin: 16px 0;
padding: 10px 20px;
}
.RTE :global(.RichEditor-editor .public-DraftStyleDefault-pre) {
background-color: rgba(0, 0, 0, 0.05);
font-family: 'Inconsolata', 'Menlo', 'Consolas', monospace;
font-size: 16px;
padding: 20px;
} }

View File

@ -39,6 +39,7 @@ class InitialStateSerializer < ActiveModel::Serializer
store[:unread_count] = unread_count object.current_account store[:unread_count] = unread_count object.current_account
store[:monthly_expenses_complete] = Redis.current.get("monthly_funding_ammount") || 0 store[:monthly_expenses_complete] = Redis.current.get("monthly_funding_ammount") || 0
store[:favourites_count] = object.current_account.favourites.count.to_s store[:favourites_count] = object.current_account.favourites.count.to_s
store[:tenorkey] = "QHFJ0C5EWGBH"
end end
store store

View File

@ -20,10 +20,10 @@ Rails.application.config.content_security_policy do |p|
if Rails.env.development? if Rails.env.development?
webpacker_urls = %w(ws http).map { |protocol| "#{protocol}#{Webpacker.dev_server.https? ? 's' : ''}://#{Webpacker.dev_server.host_with_port}" } webpacker_urls = %w(ws http).map { |protocol| "#{protocol}#{Webpacker.dev_server.https? ? 's' : ''}://#{Webpacker.dev_server.host_with_port}" }
p.connect_src :self, :blob, assets_host, Rails.configuration.x.streaming_api_base_url, *webpacker_urls, "https://*.gab.com" p.connect_src :self, :blob, assets_host, Rails.configuration.x.streaming_api_base_url, *webpacker_urls, "https://*.gab.com", "https://api.tenor.com"
p.script_src :self, :unsafe_inline, :unsafe_eval, assets_host, "https://*.gab.com" p.script_src :self, :unsafe_inline, :unsafe_eval, assets_host, "https://*.gab.com"
else else
p.connect_src :self, :blob, assets_host, Rails.configuration.x.streaming_api_base_url, "https://*.gab.com" p.connect_src :self, :blob, assets_host, Rails.configuration.x.streaming_api_base_url, "https://*.gab.com", "https://api.tenor.com"
p.script_src :self, assets_host, "https://*.gab.com" p.script_src :self, assets_host, "https://*.gab.com"
end end
end end