Added LinkTimeline and PreviewCard fetching by id
• Added: - LinkTimeline and PreviewCard fetching by id
This commit is contained in:
parent
f7dc62460c
commit
f129d9c49b
17
app/controllers/api/v1/links_controller.rb
Normal file
17
app/controllers/api/v1/links_controller.rb
Normal file
@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::LinksController < Api::BaseController
|
||||
before_action :require_user!
|
||||
before_action :set_link
|
||||
|
||||
def show
|
||||
render json: @link, serializer: REST::PreviewCardSerializer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_link
|
||||
@link = PreviewCard.find(params[:id])
|
||||
end
|
||||
|
||||
end
|
62
app/controllers/api/v1/timelines/preview_card_controller.rb
Normal file
62
app/controllers/api/v1/timelines/preview_card_controller.rb
Normal file
@ -0,0 +1,62 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Timelines::PreviewCardController < Api::BaseController
|
||||
before_action :require_user!
|
||||
before_action :set_link
|
||||
before_action :set_statuses
|
||||
|
||||
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
|
||||
|
||||
def show
|
||||
render json: @statuses,
|
||||
each_serializer: REST::StatusSerializer,
|
||||
relationships: StatusRelationshipsPresenter.new(@statuses, current_user.account_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_link
|
||||
@link = PreviewCard.find(params[:id])
|
||||
end
|
||||
|
||||
def set_statuses
|
||||
@statuses = cached_link_statuses
|
||||
end
|
||||
|
||||
def cached_link_statuses
|
||||
cache_collection link_statuses, Status
|
||||
end
|
||||
|
||||
def link_statuses
|
||||
statuses = Status.joins(
|
||||
"LEFT JOIN preview_cards_statuses ON statuses.id = preview_cards_statuses.status_id"
|
||||
).where("preview_cards_statuses.preview_card_id": params[:id]).paginate_by_id(
|
||||
limit_param(DEFAULT_STATUSES_LIMIT),
|
||||
params_slice(:max_id, :since_id, :min_id)
|
||||
).reject { |status| FeedManager.instance.filter?(:home, status, current_account.id) }
|
||||
end
|
||||
|
||||
def insert_pagination_headers
|
||||
set_pagination_headers(next_path, prev_path)
|
||||
end
|
||||
|
||||
def pagination_params(core_params)
|
||||
params.slice(:limit).permit(:limit).merge(core_params)
|
||||
end
|
||||
|
||||
def next_path
|
||||
api_v1_timelines_preview_card_url params[:id], pagination_params(max_id: pagination_max_id)
|
||||
end
|
||||
|
||||
def prev_path
|
||||
api_v1_timelines_preview_card_url params[:id], pagination_params(min_id: pagination_since_id)
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@statuses.last.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@statuses.first.id
|
||||
end
|
||||
end
|
30
app/javascript/gabsocial/actions/links.js
Normal file
30
app/javascript/gabsocial/actions/links.js
Normal file
@ -0,0 +1,30 @@
|
||||
import api from '../api'
|
||||
|
||||
export const LINK_FETCH_REQUEST = 'LINK_FETCH_REQUEST'
|
||||
export const LINK_FETCH_SUCCESS = 'LINK_FETCH_SUCCESS'
|
||||
export const LINK_FETCH_FAIL = 'LINK_FETCH_FAIL'
|
||||
|
||||
export const fetchLinkCard = (cardId) => (dispatch, getState) => {
|
||||
dispatch(fetchLinkCardRequest(cardId))
|
||||
|
||||
api(getState).get(`/api/v1/links/${cardId}`).then(({ data }) => {
|
||||
dispatch(fetchLinkCardSuccess(data))
|
||||
})
|
||||
.catch((err) => dispatch(fetchLinkCardFail(err)))
|
||||
}
|
||||
|
||||
export const fetchLinkCardRequest = (cardId) => ({
|
||||
type: LINK_FETCH_REQUEST,
|
||||
cardId,
|
||||
})
|
||||
|
||||
export const fetchLinkCardSuccess = (card) => ({
|
||||
type: LINK_FETCH_SUCCESS,
|
||||
card,
|
||||
})
|
||||
|
||||
export const fetchLinkCardFail = (error, cardId) => ({
|
||||
type: LINK_FETCH_FAIL,
|
||||
error,
|
||||
cardId,
|
||||
})
|
@ -52,6 +52,7 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null,
|
||||
|
||||
export const connectUserStream = () => connectTimelineStream('home', 'user');
|
||||
export const connectProStream = () => connectTimelineStream('pro', 'pro');
|
||||
export const connectLinkStream = (linkId, accept) => connectTimelineStream(`link:${linkId}`, `link&linkId=${linkId}`, null, accept);
|
||||
export const connectHashtagStream = (id, tag, accept) => connectTimelineStream(`hashtag:${id}`, `hashtag&tag=${tag}`, null, accept);
|
||||
export const connectListStream = id => connectTimelineStream(`list:${id}`, `list&list=${id}`);
|
||||
export const connectGroupStream = id => connectTimelineStream(`group:${id}`, `group&group=${id}`);
|
||||
|
@ -176,6 +176,7 @@ export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTim
|
||||
export const expandGroupTimeline = (id, { sortBy, maxId, onlyMedia } = {}, done = noOp) => expandTimeline(`group:${id}`, `/api/v1/timelines/group/${id}`, { sort_by: sortBy, max_id: maxId, only_media: onlyMedia }, done);
|
||||
export const expandGroupFeaturedTimeline = (groupId, done = noOp) => expandTimeline(`group:${groupId}:pinned`, `/api/v1/timelines/group_pins/${groupId}`, {}, done);
|
||||
export const expandGroupCollectionTimeline = (collectionType, { sortBy, maxId } = {}, done = noOp) => expandTimeline(`group_collection:${collectionType}`, `/api/v1/timelines/group_collection/${collectionType}`, { sort_by: sortBy, max_id: maxId }, done);
|
||||
export const expandLinkTimeline = (linkId, { maxId } = {}, done = noOp) => expandTimeline(`link:${linkId}`, `/api/v1/timelines/preview_card/${linkId}`, { max_id: maxId }, done);
|
||||
export const expandHashtagTimeline = (hashtag, { maxId, tags } = {}, done = noOp) => {
|
||||
return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, {
|
||||
max_id: maxId,
|
||||
|
108
app/javascript/gabsocial/features/link_timeline.js
Normal file
108
app/javascript/gabsocial/features/link_timeline.js
Normal file
@ -0,0 +1,108 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { connect } from 'react-redux'
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes'
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component'
|
||||
import { FormattedMessage } from 'react-intl'
|
||||
import { connectLinkStream } from '../actions/streaming'
|
||||
import { expandLinkTimeline } from '../actions/timelines'
|
||||
import { fetchLinkCard } from '../actions/links'
|
||||
import { openModal } from '../actions/modal'
|
||||
import StatusList from '../components/status_list'
|
||||
import ColumnIndicator from '../components/column_indicator'
|
||||
import Button from '../components/button'
|
||||
import Text from '../components/text'
|
||||
|
||||
class LinkTimeline extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.handleConnect(this.props.params.id)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.handleDisconnect()
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.params.id !== this.props.params.id) {
|
||||
this.handleDisconnect()
|
||||
this.handleConnect(nextProps.params.id)
|
||||
}
|
||||
}
|
||||
|
||||
handleConnect(id) {
|
||||
const { dispatch } = this.props
|
||||
|
||||
dispatch(fetchLinkCard(id))
|
||||
dispatch(expandLinkTimeline(id))
|
||||
|
||||
this.disconnect = dispatch(connectLinkStream(id))
|
||||
}
|
||||
|
||||
handleDisconnect() {
|
||||
if (this.disconnect) {
|
||||
this.disconnect()
|
||||
this.disconnect = null
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = (maxId) => {
|
||||
const { id } = this.props.params
|
||||
this.props.dispatch(expandLinkTimeline(id, { maxId }))
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
link,
|
||||
items,
|
||||
isFetched,
|
||||
isLoading,
|
||||
} = this.props
|
||||
const { id } = this.props.params
|
||||
|
||||
if (typeof link === 'undefined' && isLoading) {
|
||||
return <ColumnIndicator type='loading' />
|
||||
} else if (!link) {
|
||||
return <ColumnIndicator type='missing' />
|
||||
}
|
||||
|
||||
const emptyMessage = (
|
||||
<div className={[_s.d, _s.py15, _s.px15, _s.aiCenter].join(' ')}>
|
||||
<Text>No statuses with this url yet.</Text>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<StatusList
|
||||
scrollKey='link_timeline'
|
||||
timelineId={`link:${id}`}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={emptyMessage}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
items: state.getIn(['links', 'items']),
|
||||
link: state.getIn(['links', 'items', `${props.params.id}`]),
|
||||
isFetched: state.getIn(['links', 'isFetched']),
|
||||
isLoading: state.getIn(['links', 'isLoading']),
|
||||
})
|
||||
|
||||
LinkTimeline.propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
link: PropTypes.oneOfType([
|
||||
ImmutablePropTypes.map,
|
||||
PropTypes.bool,
|
||||
]),
|
||||
intl: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(LinkTimeline)
|
@ -44,6 +44,7 @@ import ProPage from '../../pages/pro_page'
|
||||
import ExplorePage from '../../pages/explore_page'
|
||||
import NewsPage from '../../pages/news_page'
|
||||
import AboutPage from '../../pages/about_page'
|
||||
import LinkPage from '../../pages/link_page'
|
||||
|
||||
import {
|
||||
About,
|
||||
@ -77,6 +78,7 @@ import {
|
||||
HomeTimeline,
|
||||
Investors,
|
||||
LikedStatuses,
|
||||
LinkTimeline,
|
||||
ListCreate,
|
||||
ListsDirectory,
|
||||
ListEdit,
|
||||
@ -214,6 +216,8 @@ class SwitchingArea extends React.PureComponent {
|
||||
|
||||
<WrappedRoute path='/tags/:id' publicRoute page={HashtagPage} component={HashtagTimeline} content={children} componentParams={{ title: 'Hashtag' }} />
|
||||
|
||||
<WrappedRoute path='/links/:id' page={LinkPage} component={LinkTimeline} content={children} componentParams={{ title: 'Links' }} />
|
||||
|
||||
<WrappedRoute path='/shortcuts' page={ShortcutsPage} component={Shortcuts} content={children} />
|
||||
|
||||
<WrappedRoute path='/lists' exact page={ListsPage} component={ListsDirectory} content={children} />
|
||||
|
@ -61,6 +61,7 @@ export function HotkeysModal() { return import(/* webpackChunkName: "components/
|
||||
export function Introduction() { return import(/* webpackChunkName: "features/introduction" */'../../introduction') }
|
||||
export function Investors() { return import(/* webpackChunkName: "features/about/investors" */'../../about/investors') }
|
||||
export function LinkFooter() { return import(/* webpackChunkName: "components/link_footer" */'../../../components/link_footer') }
|
||||
export function LinkTimeline() { return import(/* webpackChunkName: "features/link_timeline" */'../../link_timeline') }
|
||||
export function ListAddUserModal() { return import(/* webpackChunkName: "features/list_add_user_modal" */'../../../components/modal/list_add_user_modal') }
|
||||
export function ListCreate() { return import(/* webpackChunkName: "features/list_create" */'../../list_create') }
|
||||
export function ListCreateModal() { return import(/* webpackChunkName: "components/list_create_modal" */'../../../components/modal/list_create_modal') }
|
||||
|
46
app/javascript/gabsocial/pages/link_page.js
Normal file
46
app/javascript/gabsocial/pages/link_page.js
Normal file
@ -0,0 +1,46 @@
|
||||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import PageTitle from '../features/ui/util/page_title'
|
||||
import DefaultLayout from '../layouts/default_layout'
|
||||
import {
|
||||
LinkFooter,
|
||||
TrendsPanel,
|
||||
UserSuggestionsPanel,
|
||||
} from '../features/ui/util/async_components'
|
||||
|
||||
class LinkPage extends React.PureComponent {
|
||||
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
page,
|
||||
title,
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<DefaultLayout
|
||||
noComposeButton
|
||||
showBackBtn
|
||||
title={title}
|
||||
page={page}
|
||||
layout={[
|
||||
TrendsPanel,
|
||||
UserSuggestionsPanel,
|
||||
LinkFooter,
|
||||
]}
|
||||
>
|
||||
<PageTitle path={title} />
|
||||
{children}
|
||||
</DefaultLayout>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
LinkPage.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
page: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export default LinkPage
|
@ -15,6 +15,7 @@ import group_lists from './group_lists'
|
||||
import group_relationships from './group_relationships'
|
||||
import hashtags from './hashtags'
|
||||
import height_cache from './height_cache'
|
||||
import links from './links.js'
|
||||
import lists from './lists'
|
||||
import listAdder from './list_adder'
|
||||
import listEditor from './list_editor'
|
||||
@ -59,6 +60,7 @@ const reducers = {
|
||||
group_relationships,
|
||||
hashtags,
|
||||
height_cache,
|
||||
links,
|
||||
lists,
|
||||
listAdder,
|
||||
listEditor,
|
||||
|
40
app/javascript/gabsocial/reducers/links.js
Normal file
40
app/javascript/gabsocial/reducers/links.js
Normal file
@ -0,0 +1,40 @@
|
||||
import {
|
||||
Map as ImmutableMap,
|
||||
List as ImmutableList,
|
||||
fromJS,
|
||||
} from 'immutable'
|
||||
import {
|
||||
LINK_FETCH_REQUEST,
|
||||
LINK_FETCH_SUCCESS,
|
||||
LINK_FETCH_FAIL,
|
||||
} from '../actions/links'
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
isFetched: false,
|
||||
isError: false,
|
||||
isLoading: false,
|
||||
items: ImmutableMap(),
|
||||
})
|
||||
|
||||
export default function links(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case LINK_FETCH_REQUEST:
|
||||
return state.set('isLoading', true)
|
||||
case LINK_FETCH_SUCCESS:
|
||||
return state.withMutations((mutable) => {
|
||||
mutable.setIn(['items', `${action.card.id}`], fromJS(action.card))
|
||||
mutable.set('isLoading', false)
|
||||
mutable.set('isFetched', false)
|
||||
mutable.set('isError', false)
|
||||
})
|
||||
case LINK_FETCH_FAIL:
|
||||
return state.withMutations((mutable) => {
|
||||
mutable.set('isLoading', false)
|
||||
mutable.set('isFetched', false)
|
||||
mutable.set('isError', true)
|
||||
})
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
class REST::PreviewCardSerializer < ActiveModel::Serializer
|
||||
include RoutingHelper
|
||||
|
||||
attributes :url, :title, :description, :type,
|
||||
attributes :id, :url, :title, :description, :type,
|
||||
:author_name, :author_url, :provider_name,
|
||||
:provider_url, :html, :width, :height,
|
||||
:image, :embed_url
|
||||
|
@ -331,10 +331,12 @@ Rails.application.routes.draw do
|
||||
resources :group, only: :show
|
||||
resources :group_collection, only: :show
|
||||
resources :group_pins, only: :show
|
||||
resources :preview_card, only: :show
|
||||
resource :explore, only: :show, controller: :explore
|
||||
end
|
||||
|
||||
resources :gab_trends, only: [:index]
|
||||
resources :links, only: :show
|
||||
resources :shop, only: [:index]
|
||||
resources :streaming, only: [:index]
|
||||
resources :custom_emojis, only: [:index]
|
||||
|
@ -88,6 +88,8 @@ defaults: &defaults
|
||||
- welcome
|
||||
- bookmarks
|
||||
- suggestions
|
||||
- link
|
||||
- links
|
||||
disallowed_hashtags: # space separated string or list of hashtags without the hash
|
||||
bootstrap_timeline_accounts: ''
|
||||
activity_api_enabled: true
|
||||
|
@ -623,6 +623,14 @@ const startWorker = (workerId) => {
|
||||
|
||||
streamFrom(`timeline:hashtag:${location.query.tag.toLowerCase()}`, req, streamToWs(req, ws), streamWsEnd(req, ws), true);
|
||||
break;
|
||||
case 'link':
|
||||
if (!location.query.linkId || location.query.linkId.length === 0) {
|
||||
ws.close();
|
||||
return;
|
||||
}
|
||||
|
||||
streamFrom(`timeline:link:${location.query.linkId}`, req, streamToWs(req, ws), streamWsEnd(req, ws), true);
|
||||
break;
|
||||
case 'hashtag:local':
|
||||
if (!location.query.tag || location.query.tag.length === 0) {
|
||||
ws.close();
|
||||
|
Loading…
Reference in New Issue
Block a user