@@ -5,16 +5,16 @@ require 'nokogiri' | |||||
class PostsController < ApplicationController | class PostsController < ApplicationController | ||||
# GET /posts | # GET /posts | ||||
def index | def index | ||||
limit = (params[:limit] || 20).to_i | |||||
limit = params[:limit].presence&.to_i | |||||
cursor = params[:cursor].presence | cursor = params[:cursor].presence | ||||
q = filtered_posts.order(created_at: :desc) | q = filtered_posts.order(created_at: :desc) | ||||
q = q.where('posts.created_at < ?', Time.iso8601(cursor)) if cursor | q = q.where('posts.created_at < ?', Time.iso8601(cursor)) if cursor | ||||
posts = q.limit(limit + 1) | |||||
posts = limit ? q.limit(limit + 1) : q | |||||
next_cursor = nil | next_cursor = nil | ||||
if posts.size > limit | |||||
if limit && posts.size > limit | |||||
next_cursor = posts.last.created_at.iso8601(6) | next_cursor = posts.last.created_at.iso8601(6) | ||||
posts = posts.first(limit) | posts = posts.first(limit) | ||||
end | end | ||||
@@ -51,7 +51,7 @@ class PostsController < ApplicationController | |||||
render json: (post | render json: (post | ||||
.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }) | .as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }) | ||||
.merge(viewed: viewed)) | |||||
.merge(related: post.related(limit: 20), viewed:)) | |||||
end | end | ||||
# POST /posts | # POST /posts | ||||
@@ -7,6 +7,12 @@ class Post < ApplicationRecord | |||||
has_many :post_tags, dependent: :destroy | has_many :post_tags, dependent: :destroy | ||||
has_many :tags, through: :post_tags | has_many :tags, through: :post_tags | ||||
has_many :user_post_views, dependent: :destroy | has_many :user_post_views, dependent: :destroy | ||||
has_many :post_similarities_as_post, | |||||
class_name: 'PostSimilarity', | |||||
foreign_key: :post_id | |||||
has_many :post_similarities_as_target_post, | |||||
class_name: 'PostSimilarity', | |||||
foreign_key: :target_post_id | |||||
has_one_attached :thumbnail | has_one_attached :thumbnail | ||||
def as_json options = { } | def as_json options = { } | ||||
@@ -18,6 +24,21 @@ class Post < ApplicationRecord | |||||
super(options).merge(thumbnail: nil) | super(options).merge(thumbnail: nil) | ||||
end | end | ||||
def related(limit: nil) | |||||
ids_with_cos = | |||||
post_similarities_as_post.select(:target_post_id, :cos) | |||||
.map { |ps| [ps.target_post_id, ps.cos] } + | |||||
post_similarities_as_target_post.select(:post_id, :cos) | |||||
.map { |ps| [ps.post_id, ps.cos] } | |||||
sorted = ids_with_cos.sort_by { |_, cos| -cos } | |||||
ids = sorted.map(&:first) | |||||
ids = ids.first(limit) if limit | |||||
Post.where(id: ids) | |||||
end | |||||
def resized_thumbnail! | def resized_thumbnail! | ||||
return unless thumbnail.attached? | return unless thumbnail.attached? | ||||
@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react' | |||||
import { Helmet } from 'react-helmet-async' | import { Helmet } from 'react-helmet-async' | ||||
import { useParams } from 'react-router-dom' | import { useParams } from 'react-router-dom' | ||||
import PostList from '@/components/PostList' | |||||
import TagDetailSidebar from '@/components/TagDetailSidebar' | import TagDetailSidebar from '@/components/TagDetailSidebar' | ||||
import NicoViewer from '@/components/NicoViewer' | import NicoViewer from '@/components/NicoViewer' | ||||
import PostEditForm from '@/components/PostEditForm' | import PostEditForm from '@/components/PostEditForm' | ||||
@@ -117,6 +118,9 @@ export default ({ user }: Props) => { | |||||
{post.viewed ? '閲覧済' : '未閲覧'} | {post.viewed ? '閲覧済' : '未閲覧'} | ||||
</Button> | </Button> | ||||
<TabGroup> | <TabGroup> | ||||
<Tab name="関聯"> | |||||
<PostList posts={post.related} /> | |||||
</Tab> | |||||
{(['admin', 'member'].some (r => user?.role === r) && editing) && ( | {(['admin', 'member'].some (r => user?.role === r) && editing) && ( | ||||
<Tab name="編輯"> | <Tab name="編輯"> | ||||
<PostEditForm post={post} | <PostEditForm post={post} | ||||
@@ -23,7 +23,8 @@ export type Post = { | |||||
thumbnail: string | thumbnail: string | ||||
thumbnailBase: string | thumbnailBase: string | ||||
tags: Tag[] | tags: Tag[] | ||||
viewed: boolean } | |||||
viewed: boolean | |||||
related: Post[] } | |||||
export type SubMenuItem = { | export type SubMenuItem = { | ||||
component: React.ReactNode | component: React.ReactNode | ||||