Browse Source

Merge branch 'feature/188' of https://git.miteruzo.com/miteruzo/btrc-hub into feature/188

feature/188
みてるぞ 3 days ago
parent
commit
592648c747
3 changed files with 76 additions and 28 deletions
  1. +25
    -10
      backend/app/controllers/posts_controller.rb
  2. +24
    -5
      frontend/src/components/PostEmbed.tsx
  3. +27
    -13
      frontend/src/pages/posts/PostListPage.tsx

+ 25
- 10
backend/app/controllers/posts_controller.rb View File

@@ -3,20 +3,35 @@ class PostsController < ApplicationController

# GET /posts
def index
limit = params[:limit].presence&.to_i
page = (params[:page].presence || 1).to_i
limit = (params[:limit].presence || 20).to_i
cursor = params[:cursor].presence

created_at = ('COALESCE(posts.original_created_before - INTERVAL 1 SECOND,' +
'posts.original_created_from,' +
'posts.created_at)')
q = filtered_posts.order(Arel.sql("#{ created_at } DESC"))
q = q.where("#{ created_at } < ?", Time.iso8601(cursor)) if cursor
page = 1 if page < 1
limit = 1 if limit < 1

offset = (page - 1) * limit

posts = limit ? q.limit(limit + 1) : q
sort_sql =
'COALESCE(posts.original_created_before - INTERVAL 1 SECOND,' +
'posts.original_created_from,' +
'posts.created_at)'
q =
filtered_posts
.preload(:tags)
.with_attached_thumbnail
.select("posts.*, #{ sort_sql } AS sort_ts")
.order(Arel.sql("#{ sort_sql } DESC"))
posts = (
if cursor
q.where("#{ sort_sql } < ?", Time.iso8601(cursor)).limit(limit + 1)
else
q.limit(limit).offset(offset)
end).to_a

next_cursor = nil
if limit && posts.size > limit
next_cursor = posts.last.created_at.iso8601(6)
if cursor && posts.length > limit
next_cursor = posts.last.read_attribute('sort_ts').iso8601(6)
posts = posts.first(limit)
end

@@ -29,7 +44,7 @@ class PostsController < ApplicationController
nil
end
end
}, next_cursor: }
}, count: filtered_posts.count(:id), next_cursor: }
end

def random


+ 24
- 5
frontend/src/components/PostEmbed.tsx View File

@@ -1,3 +1,4 @@
import { useState } from 'react'
import YoutubeEmbed from 'react-youtube'

import NicoViewer from '@/components/NicoViewer'
@@ -39,10 +40,28 @@ export default (({ post }: Props) => {
}
}

const [framed, setFramed] = useState (false)

return (
<a href={post.url} target="_blank">
<img src={post.thumbnailBase || post.thumbnail}
alt={post.url}
className="mb-4 w-full"/>
</a>)
<>
{framed
? (
<iframe
src={post.url}
title={post.title || post.url}
width={640}
height={360}/>)
: (
<div>
<a href="#" onClick={e => {
e.preventDefault ()
setFramed (confirm ('未確認の外部ページを表示します。\n'
+ '悪意のあるスクリプトが実行される可能性があります。\n'
+ '表示しますか?'))
return
}}>
外部ページを表示
</a>
</div>)}
</>)
}) satisfies FC<Props>

+ 27
- 13
frontend/src/pages/posts/PostListPage.tsx View File

@@ -7,6 +7,7 @@ import { Link, useLocation, useNavigationType } from 'react-router-dom'
import PostList from '@/components/PostList'
import TagSidebar from '@/components/TagSidebar'
import WikiBody from '@/components/WikiBody'
import Pagination from '@/components/common/Pagination'
import TabGroup, { Tab } from '@/components/common/TabGroup'
import MainArea from '@/components/layout/MainArea'
import { API_BASE_URL, SITE_TITLE } from '@/config'
@@ -23,6 +24,7 @@ export default () => {
const [cursor, setCursor] = useState ('')
const [loading, setLoading] = useState (false)
const [posts, setPosts] = useState<Post[]> ([])
const [totalPages, setTotalPages] = useState (0)
const [wikiPage, setWikiPage] = useState<WikiPage | null> (null)

const loadMore = async (withCursor: boolean) => {
@@ -31,15 +33,19 @@ export default () => {
const res = await axios.get (`${ API_BASE_URL }/posts`, {
params: { tags: tags.join (' '),
match: anyFlg ? 'any' : 'all',
limit: '20',
...(page && { page }),
...(limit && { limit }),
...(withCursor && { cursor }) } })
const data = toCamel (res.data as any, { deep: true }) as { posts: Post[]
nextCursor: string }
const data = toCamel (res.data as any, { deep: true }) as {
posts: Post[]
count: number
nextCursor: string }
setPosts (posts => (
[...((new Map ([...(withCursor ? posts : []), ...data.posts]
.map (post => [post.id, post])))
.values ())]))
setCursor (data.nextCursor)
setTotalPages (Math.ceil (data.count / limit))

setLoading (false)
}
@@ -49,6 +55,8 @@ export default () => {
const tagsQuery = query.get ('tags') ?? ''
const anyFlg = query.get ('match') === 'any'
const tags = tagsQuery.split (' ').filter (e => e !== '')
const page = Number (query.get ('page') ?? 1)
const limit = Number (query.get ('limit') ?? 20)

useEffect(() => {
const observer = new IntersectionObserver (entries => {
@@ -65,7 +73,8 @@ export default () => {
}, [loaderRef, loading])

useLayoutEffect (() => {
const savedState = sessionStorage.getItem (`posts:${ tagsQuery }`)
// TODO: 無限ロード用
const savedState = /* sessionStorage.getItem (`posts:${ tagsQuery }`) */ null
if (savedState && navigationType === 'POP')
{
const { posts, cursor, scroll } = JSON.parse (savedState)
@@ -116,18 +125,23 @@ export default () => {
<MainArea>
<TabGroup>
<Tab name="広場">
{posts.length
{posts.length > 0
? (
<PostList posts={posts} onClick={() => {
const statesToSave = {
posts, cursor,
scroll: containerRef.current?.scrollTop ?? 0 }
sessionStorage.setItem (`posts:${ tagsQuery }`,
JSON.stringify (statesToSave))
}}/>)
<>
<PostList posts={posts} onClick={() => {
// TODO: 無限ロード用なので復活時に戻す.
// const statesToSave = {
// posts, cursor,
// scroll: containerRef.current?.scrollTop ?? 0 }
// sessionStorage.setItem (`posts:${ tagsQuery }`,
// JSON.stringify (statesToSave))
}}/>
<Pagination page={page} totalPages={totalPages}/>
</>)
: !(loading) && '広場には何もありませんよ.'}
{loading && 'Loading...'}
<div ref={loaderRef} className="h-12"/>
{/* TODO: 無限ローディング復活までコメント・アウト */}
{/* <div ref={loaderRef} className="h-12"/> */}
</Tab>
{tags.length === 1 && (
<Tab name="Wiki">


Loading…
Cancel
Save