Commit 81e8b329 authored by Rob Colbert's avatar Rob Colbert

Merge branch 'develop' of https://code.gab.com/gab/social/gab-social into develop

parents 29f324d4 d382c557
...@@ -19,10 +19,10 @@ We have diverged from Mastodon in several ways in pursuit of our own goals. ...@@ -19,10 +19,10 @@ We have diverged from Mastodon in several ways in pursuit of our own goals.
1. Quote posting 1. Quote posting
## BTCPay ## BTCPay
In order to make BTC flow work, 3 enviornment variables need to be set: In order to make BTC flow work, 3 environment variables need to be set:
- `BTCPAY_LEGACY_TOKEN`: So called Legacy Tokens can be found in https://btcpay.xxx.com/stores/yyy/Tokens - `BTCPAY_LEGACY_TOKEN`: So called Legacy Tokens can be found in https://btcpay.[yourdomain].com/stores/[yourstore]/Tokens
- `BTCPAY_PUB_KEY`: Public key that is used when creating an access token or pairing https://btcpay.xxx.com/stores/yyy/Tokens/Create - `BTCPAY_PUB_KEY`: Public key that is used when creating an access token or pairing https://btcpay.[yourdomain].com/stores/[yourstore]/Tokens/Create
- `BTCPAY_MERCHANT_TOKEN`: Token created for facade *merchant* - `BTCPAY_MERCHANT_TOKEN`: Token created for facade *merchant*
## Deployment ## Deployment
......
...@@ -137,6 +137,32 @@ export function directCompose(account, routerHistory) { ...@@ -137,6 +137,32 @@ export function directCompose(account, routerHistory) {
}; };
}; };
export function handleComposeSubmit(dispatch, getState, response, status) {
if (!dispatch || !getState) return;
dispatch(insertIntoTagHistory(response.data.tags, status));
dispatch(submitComposeSuccess({ ...response.data }));
// To make the app more responsive, immediately push the status into the columns
const insertIfOnline = timelineId => {
const timeline = getState().getIn(['timelines', timelineId]);
if (timeline && timeline.get('items').size > 0 && timeline.getIn(['items', 0]) !== null && timeline.get('online')) {
let dequeueArgs = {};
if (timelineId === 'community') dequeueArgs.onlyMedia = getState().getIn(['settings', 'community', 'other', 'onlyMedia']);
dispatch(dequeueTimeline(timelineId, null, dequeueArgs));
dispatch(updateTimeline(timelineId, { ...response.data }));
}
};
if (response.data.visibility !== 'direct') {
insertIfOnline('home');
} else if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
insertIfOnline('community');
insertIfOnline('public');
}
}
export function submitCompose(routerHistory, group) { export function submitCompose(routerHistory, group) {
return function (dispatch, getState) { return function (dispatch, getState) {
if (!me) return; if (!me) return;
...@@ -175,33 +201,7 @@ export function submitCompose(routerHistory, group) { ...@@ -175,33 +201,7 @@ export function submitCompose(routerHistory, group) {
if (response.data.visibility === 'direct' && getState().getIn(['conversations', 'mounted']) <= 0 && routerHistory) { if (response.data.visibility === 'direct' && getState().getIn(['conversations', 'mounted']) <= 0 && routerHistory) {
routerHistory.push('/messages'); routerHistory.push('/messages');
} }
handleComposeSubmit(dispatch, getState, response, status);
dispatch(insertIntoTagHistory(response.data.tags, status));
dispatch(submitComposeSuccess({ ...response.data }));
// To make the app more responsive, immediately push the status
// into the columns
const insertIfOnline = timelineId => {
const timeline = getState().getIn(['timelines', timelineId]);
if (timeline && timeline.get('items').size > 0 && timeline.getIn(['items', 0]) !== null && timeline.get('online')) {
let dequeueArgs = {};
if (timelineId === 'community') dequeueArgs.onlyMedia = getState().getIn(['settings', 'community', 'other', 'onlyMedia']),
dispatch(dequeueTimeline(timelineId, null, dequeueArgs));
dispatch(updateTimeline(timelineId, { ...response.data }));
}
};
if (response.data.visibility !== 'direct') {
insertIfOnline('home');
}
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
insertIfOnline('community');
insertIfOnline('public');
}
}).catch(function (error) { }).catch(function (error) {
dispatch(submitComposeFail(error)); dispatch(submitComposeFail(error));
}); });
......
...@@ -10,6 +10,7 @@ import { updateNotificationsQueue, expandNotifications } from './notifications'; ...@@ -10,6 +10,7 @@ import { updateNotificationsQueue, expandNotifications } from './notifications';
import { updateConversations } from './conversations'; import { updateConversations } from './conversations';
import { fetchFilters } from './filters'; import { fetchFilters } from './filters';
import { getLocale } from '../locales'; import { getLocale } from '../locales';
import { handleComposeSubmit } from './compose';
const { messages } = getLocale(); const { messages } = getLocale();
...@@ -61,3 +62,18 @@ export const connectHashtagStream = (id, tag, accept) => connectTimelineStream ...@@ -61,3 +62,18 @@ export const connectHashtagStream = (id, tag, accept) => connectTimelineStream
export const connectDirectStream = () => connectTimelineStream('direct', 'direct'); export const connectDirectStream = () => connectTimelineStream('direct', 'direct');
export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`); export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);
export const connectGroupStream = id => connectTimelineStream(`group:${id}`, `group&group=${id}`); export const connectGroupStream = id => connectTimelineStream(`group:${id}`, `group&group=${id}`);
export const connectStatusUpdateStream = () => {
return connectStream('statuscard', null, (dispatch, getState) => {
return {
onConnect() {},
onDisconnect() {},
onReceive (data) {
if (!data['event'] || !data['payload']) return;
if (data.event === 'update') {
handleComposeSubmit(dispatch, getState, {data: JSON.parse(data.payload)}, null)
}
},
};
});
}
\ No newline at end of file
...@@ -44,9 +44,9 @@ export default class ExtendedVideoPlayer extends React.PureComponent { ...@@ -44,9 +44,9 @@ export default class ExtendedVideoPlayer extends React.PureComponent {
return ( return (
<div className='extended-video-player'> <div className='extended-video-player'>
<video <video
playsInline
ref={this.setRef} ref={this.setRef}
src={src} src={src}
autoPlay
role='button' role='button'
tabIndex='0' tabIndex='0'
aria-label={alt} aria-label={alt}
......
...@@ -189,6 +189,7 @@ class Item extends React.PureComponent { ...@@ -189,6 +189,7 @@ class Item extends React.PureComponent {
autoPlay={autoPlay} autoPlay={autoPlay}
loop loop
muted muted
playsInline
/> />
<span className='media-gallery__gifv__label'>GIF</span> <span className='media-gallery__gifv__label'>GIF</span>
......
...@@ -380,7 +380,6 @@ class Status extends ImmutablePureComponent { ...@@ -380,7 +380,6 @@ class Status extends ImmutablePureComponent {
<Card <Card
onOpenMedia={this.props.onOpenMedia} onOpenMedia={this.props.onOpenMedia}
card={status.get('card')} card={status.get('card')}
compact
cacheWidth={this.props.cacheMediaWidth} cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth} defaultWidth={this.props.cachedMediaWidth}
/> />
......
...@@ -11,7 +11,10 @@ import UI from '../features/ui'; ...@@ -11,7 +11,10 @@ import UI from '../features/ui';
import Introduction from '../features/introduction'; import Introduction from '../features/introduction';
import { fetchCustomEmojis } from '../actions/custom_emojis'; import { fetchCustomEmojis } from '../actions/custom_emojis';
import { hydrateStore } from '../actions/store'; import { hydrateStore } from '../actions/store';
import { connectUserStream } from '../actions/streaming'; import {
connectUserStream,
connectStatusUpdateStream,
} from '../actions/streaming';
import { IntlProvider, addLocaleData } from 'react-intl'; import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales'; import { getLocale } from '../locales';
import initialState from '../initial_state'; import initialState from '../initial_state';
...@@ -70,6 +73,7 @@ export default class GabSocial extends React.PureComponent { ...@@ -70,6 +73,7 @@ export default class GabSocial extends React.PureComponent {
componentDidMount() { componentDidMount() {
this.disconnect = store.dispatch(connectUserStream()); this.disconnect = store.dispatch(connectUserStream());
store.dispatch(connectStatusUpdateStream());
} }
componentWillUnmount () { componentWillUnmount () {
......
...@@ -127,6 +127,7 @@ export default class MediaItem extends ImmutablePureComponent { ...@@ -127,6 +127,7 @@ export default class MediaItem extends ImmutablePureComponent {
autoPlay={autoPlay} autoPlay={autoPlay}
loop loop
muted muted
playsInline
/> />
<span className='media-gallery__gifv__label'>GIF</span> <span className='media-gallery__gifv__label'>GIF</span>
......
...@@ -58,16 +58,12 @@ export default class Card extends React.PureComponent { ...@@ -58,16 +58,12 @@ export default class Card extends React.PureComponent {
static propTypes = { static propTypes = {
card: ImmutablePropTypes.map, card: ImmutablePropTypes.map,
maxDescription: PropTypes.number,
onOpenMedia: PropTypes.func.isRequired, onOpenMedia: PropTypes.func.isRequired,
compact: PropTypes.bool,
defaultWidth: PropTypes.number, defaultWidth: PropTypes.number,
cacheWidth: PropTypes.func, cacheWidth: PropTypes.func,
}; };
static defaultProps = { static defaultProps = {
maxDescription: 50,
compact: false,
}; };
state = { state = {
...@@ -131,37 +127,52 @@ export default class Card extends React.PureComponent { ...@@ -131,37 +127,52 @@ export default class Card extends React.PureComponent {
ref={this.setRef} ref={this.setRef}
className='status-card__image status-card-video' className='status-card__image status-card-video'
dangerouslySetInnerHTML={content} dangerouslySetInnerHTML={content}
style={{ height }} style={{
height,
paddingBottom: 0,
}}
/> />
); );
} }
render () { render () {
const { card, maxDescription, compact } = this.props; const { card } = this.props;
const { width, embedded } = this.state; const { width, embedded } = this.state;
if (card === null) { if (card === null) {
return null; return null;
} }
const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name'); const maxDescription = 150;
const horizontal = (!compact && card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded; const cardImg = card.get('image');
const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name');
const horizontal = (card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded;
const interactive = card.get('type') !== 'link'; const interactive = card.get('type') !== 'link';
const className = classnames('status-card', { horizontal, compact, interactive }); const className = classnames('status-card', {
const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>; horizontal,
const ratio = card.get('width') / card.get('height'); interactive,
const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio); compact: !cardImg,
});
const title = interactive ?
<a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener' target='_blank'>
<strong>{card.get('title')}</strong>
</a>
: <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
const description = ( const description = (
<div className='status-card__content'> <div className='status-card__content'>
{title} {title}
{!(horizontal || compact) && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>} {!horizontal && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>}
<span className='status-card__host'>{provider}</span> <span className='status-card__host'>
<Icon id='link' fixedWidth />
{' '}
{provider}
</span>
</div> </div>
); );
let embed = ''; let embed = '';
let thumbnail = <div style={{ backgroundImage: `url(${card.get('image')})`, width: horizontal ? width : null, height: horizontal ? height : null }} className='status-card__image-image' />; let thumbnail = <div style={{ backgroundImage: `url(${cardImg})` }} className='status-card__image-image' />;
if (interactive) { if (interactive) {
if (embedded) { if (embedded) {
...@@ -176,7 +187,6 @@ export default class Card extends React.PureComponent { ...@@ -176,7 +187,6 @@ export default class Card extends React.PureComponent {
embed = ( embed = (
<div className='status-card__image'> <div className='status-card__image'>
{thumbnail} {thumbnail}
<div className='status-card__actions'> <div className='status-card__actions'>
<div> <div>
<button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button> <button onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button>
...@@ -190,10 +200,10 @@ export default class Card extends React.PureComponent { ...@@ -190,10 +200,10 @@ export default class Card extends React.PureComponent {
return ( return (
<div className={className} ref={this.setRef}> <div className={className} ref={this.setRef}>
{embed} {embed}
{!compact && description} {description}
</div> </div>
); );
} else if (card.get('image')) { } else if (cardImg) {
embed = ( embed = (
<div className='status-card__image'> <div className='status-card__image'>
{thumbnail} {thumbnail}
......
...@@ -344,6 +344,7 @@ class Video extends React.PureComponent { ...@@ -344,6 +344,7 @@ class Video extends React.PureComponent {
} }
handleProgress = () => { handleProgress = () => {
if (!this.video.buffered) return;
if (this.video.buffered.length > 0) { if (this.video.buffered.length > 0) {
this.setState({ buffer: this.video.buffered.end(0) / this.video.duration * 100 }); this.setState({ buffer: this.video.buffered.end(0) / this.video.duration * 100 });
} }
...@@ -432,6 +433,7 @@ class Video extends React.PureComponent { ...@@ -432,6 +433,7 @@ class Video extends React.PureComponent {
<canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': revealed })} /> <canvas width={32} height={32} ref={this.setCanvasRef} className={classNames('media-gallery__preview', { 'media-gallery__preview--hidden': revealed })} />
{revealed && <video {revealed && <video
playsInline
ref={this.setVideoRef} ref={this.setVideoRef}
src={src} src={src}
poster={preview} poster={preview}
......
...@@ -2192,13 +2192,14 @@ a.account__display-name { ...@@ -2192,13 +2192,14 @@ a.account__display-name {
.status-card { .status-card {
display: flex; display: flex;
font-size: 14px; font-size: 15px;
border: 1px solid lighten($ui-base-color, 8%); border: 1px solid lighten($ui-base-color, 12%);
border-radius: 4px; border-radius: 4px;
color: $dark-text-color; color: $dark-text-color;
margin-top: 14px; margin-top: 14px;
text-decoration: none; text-decoration: none;
overflow: hidden; overflow: hidden;
flex-direction: column;
&__actions { &__actions {
bottom: 0; bottom: 0;
...@@ -2274,7 +2275,7 @@ a.status-card { ...@@ -2274,7 +2275,7 @@ a.status-card {
display: block; display: block;
font-weight: 500; font-weight: 500;
margin-bottom: 5px; margin-bottom: 5px;
color: $darker-text-color; color: $primary-text-color;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
...@@ -2284,26 +2285,33 @@ a.status-card { ...@@ -2284,26 +2285,33 @@ a.status-card {
.status-card__content { .status-card__content {
flex: 1 1 auto; flex: 1 1 auto;
overflow: hidden; overflow: hidden;
padding: 14px 14px 14px 8px; padding: 12px 10px;
border-top: 1px solid lighten($ui-base-color, 12%);
} }
.status-card__description { .status-card__description {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
max-height: 40px;
overflow: hidden;
color: $darker-text-color; color: $darker-text-color;
} }
.status-card__host { .status-card__host {
display: block; display: block;
margin-top: 5px; margin-top: 5px;
font-size: 13px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
color: $darker-text-color;
} }
.status-card__image { .status-card__image {
flex: 0 0 100px; display: block;
background: lighten($ui-base-color, 8%); padding-bottom: 52.25%;
position: relative; position: relative;
background: lighten($ui-base-color, 8%);
& > .fa { & > .fa {
font-size: 21px; font-size: 21px;
...@@ -2332,7 +2340,7 @@ a.status-card { ...@@ -2332,7 +2340,7 @@ a.status-card {
} }
.status-card.compact { .status-card.compact {
border-color: lighten($ui-base-color, 4%); flex-direction: row;
&.interactive { &.interactive {
border: 0; border: 0;
...@@ -2348,7 +2356,8 @@ a.status-card { ...@@ -2348,7 +2356,8 @@ a.status-card {
} }
.status-card__image { .status-card__image {
flex: 0 0 60px; flex: 0 0 94px;
padding-bottom: 0;
} }
} }
...@@ -2357,11 +2366,13 @@ a.status-card.compact:hover { ...@@ -2357,11 +2366,13 @@ a.status-card.compact:hover {
} }
.status-card__image-image { .status-card__image-image {
border-radius: 4px 0 0 4px;
display: block; display: block;
position: absolute;
right: 0;
left: 0;
top: 0;
bottom: 0;
margin: 0; margin: 0;
width: 100%;
height: 100%;
object-fit: cover; object-fit: cover;
background-size: cover; background-size: cover;
background-position: center center; background-position: center center;
......
...@@ -15,7 +15,15 @@ class FetchLinkCardService < BaseService ...@@ -15,7 +15,15 @@ class FetchLinkCardService < BaseService
@status = status @status = status
@url = parse_urls @url = parse_urls
return if @url.nil? || @status.preview_cards.any? if @status.preview_cards.any?
if @url.nil?
detach_card
return
end
return if @status.preview_cards.first.url == @url
end
return if @url.nil?
@url = @url.to_s @url = @url.to_s
...@@ -39,12 +47,6 @@ class FetchLinkCardService < BaseService ...@@ -39,12 +47,6 @@ class FetchLinkCardService < BaseService
def process_url def process_url
@card ||= PreviewCard.new(url: @url) @card ||= PreviewCard.new(url: @url)
failed = Request.new(:head, @url).perform do |res|
res.code != 405 && res.code != 501 && (res.code != 200 || res.mime_type != 'text/html')
end
return if failed
Request.new(:get, @url).perform do |res| Request.new(:get, @url).perform do |res|
if res.code == 200 && res.mime_type == 'text/html' if res.code == 200 && res.mime_type == 'text/html'
@html = res.body_with_limit @html = res.body_with_limit
...@@ -55,13 +57,23 @@ class FetchLinkCardService < BaseService ...@@ -55,13 +57,23 @@ class FetchLinkCardService < BaseService
end end
end end
return if @html.nil? if @html.nil?
detach_card
return
end
attempt_oembed || attempt_opengraph attempt_oembed || attempt_opengraph
end end
def attach_card def attach_card
@status.preview_cards << @card @status.preview_cards = [@card]
send_status_update_payload(@status)
Rails.cache.delete(@status)
end
def detach_card
@status.preview_cards = []
send_status_update_payload(@status)
Rails.cache.delete(@status) Rails.cache.delete(@status)
end end
...@@ -171,4 +183,10 @@ class FetchLinkCardService < BaseService ...@@ -171,4 +183,10 @@ class FetchLinkCardService < BaseService
def lock_options def lock_options
{ redis: Redis.current, key: "fetch:#{@url}" } { redis: Redis.current, key: "fetch:#{@url}" }
end end
def send_status_update_payload(status)
@payload = InlineRenderer.render(status, nil, :status)
@payload = Oj.dump(event: :update, payload: @payload)
Redis.current.publish('statuscard', @payload)
end
end end
...@@ -534,6 +534,11 @@ const startWorker = (workerId) => { ...@@ -534,6 +534,11 @@ const startWorker = (workerId) => {
app.use(authenticationMiddleware); app.use(authenticationMiddleware);
app.use(errorMiddleware); app.use(errorMiddleware);
app.get('/api/v1/streaming/statuscard', (req, res) => {
const channel = `statuscard`;
streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req, subscriptionHeartbeat(channel)));
});
app.get('/api/v1/streaming/user', (req, res) => { app.get('/api/v1/streaming/user', (req, res) => {
const channel = `timeline:${req.accountId}`; const channel = `timeline:${req.accountId}`;
streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req, subscriptionHeartbeat(channel))); streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req, subscriptionHeartbeat(channel)));
...@@ -608,6 +613,10 @@ const startWorker = (workerId) => { ...@@ -608,6 +613,10 @@ const startWorker = (workerId) => {
let channel; let channel;
switch(location.query.stream) { switch(location.query.stream) {
case 'statuscard':
channel = `statuscard`;
streamFrom(channel, req, streamToWs(req, ws), streamWsEnd(req, ws, subscriptionHeartbeat(channel)));
break;
case 'user': case 'user':
channel = `timeline:${req.accountId}`; channel = `timeline:${req.accountId}`;
streamFrom(channel, req, streamToWs(req, ws), streamWsEnd(req, ws, subscriptionHeartbeat(channel)));