Added LinkTimeline and PreviewCard fetching by id

• Added:
- LinkTimeline and PreviewCard fetching by id
This commit is contained in:
mgabdev 2020-10-29 18:46:54 -05:00
parent f7dc62460c
commit f129d9c49b
15 changed files with 325 additions and 1 deletions

View 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

View 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

View 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,
})

View File

@ -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}`);

View File

@ -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,

View 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)

View File

@ -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,
@ -213,6 +215,8 @@ class SwitchingArea extends React.PureComponent {
<WrappedRoute path='/groups/:id' exact publicRoute page={GroupPage} component={GroupTimeline} content={children} componentParams={{ isTimeline: true }} />
<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} />

View File

@ -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') }

View 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

View File

@ -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,

View 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
}
}

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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();