Browse Source

#297

feature/297
みてるぞ 3 days ago
parent
commit
63c1dd197c
4 changed files with 61 additions and 43 deletions
  1. +1
    -1
      backend/app/controllers/theatre_comments_controller.rb
  2. +7
    -7
      backend/spec/requests/theatre_comments_spec.rb
  3. +20
    -2
      backend/spec/requests/theatres_spec.rb
  4. +33
    -33
      frontend/src/pages/theatres/TheatreDetailPage.tsx

+ 1
- 1
backend/app/controllers/theatre_comments_controller.rb View File

@@ -6,7 +6,7 @@ class TheatreCommentsController < ApplicationController
comments = TheatreComment comments = TheatreComment
.where(theatre_id: params[:theatre_id]) .where(theatre_id: params[:theatre_id])
.where('no > ?', no_gt) .where('no > ?', no_gt)
.order(:no)
.order(no: :desc)


render json: comments.as_json(include: { user: { only: [:id, :name] } }) render json: comments.as_json(include: { user: { only: [:id, :name] } })
end end


+ 7
- 7
backend/spec/requests/theatre_comments_spec.rb View File

@@ -51,14 +51,14 @@ RSpec.describe 'TheatreComments', type: :request do
) )
end end


it 'theatre_id で絞り込み、no_gt より大きいものを no 順で返す' do
it 'theatre_id で絞り込み、no_gt より大きいものを no 順で返す' do
get "/theatres/#{theatre.id}/comments", params: { no_gt: 1 } get "/theatres/#{theatre.id}/comments", params: { no_gt: 1 }


expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(response.parsed_body.map { |row| row['no'] }).to eq([2, 3])
expect(response.parsed_body.map { |row| row['no'] }).to eq([3, 2])
expect(response.parsed_body.map { |row| row['content'] }).to eq([ expect(response.parsed_body.map { |row| row['content'] }).to eq([
'second comment',
'third comment'
'third comment',
'second comment'
]) ])
end end


@@ -68,8 +68,8 @@ RSpec.describe 'TheatreComments', type: :request do
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)


expect(response.parsed_body.first['user']).to eq({ expect(response.parsed_body.first['user']).to eq({
'id' => bob.id,
'name' => 'Bob'
'id' => alice.id,
'name' => 'Alice'
}) })
expect(response.parsed_body.first['user'].keys).to contain_exactly('id', 'name') expect(response.parsed_body.first['user'].keys).to contain_exactly('id', 'name')
end end
@@ -78,7 +78,7 @@ RSpec.describe 'TheatreComments', type: :request do
get "/theatres/#{theatre.id}/comments", params: { no_gt: -100 } get "/theatres/#{theatre.id}/comments", params: { no_gt: -100 }


expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(response.parsed_body.map { |row| row['no'] }).to eq([1, 2, 3])
expect(response.parsed_body.map { |row| row['no'] }).to eq([3, 2, 1])
end end
end end




+ 20
- 2
backend/spec/requests/theatres_spec.rb View File

@@ -117,11 +117,18 @@ RSpec.describe 'Theatres API', type: :request do
expect(theatre.host_user_id).to eq(member.id) expect(theatre.host_user_id).to eq(member.id)
expect(watch.expires_at).to be_within(1.second).of(30.seconds.from_now) expect(watch.expires_at).to be_within(1.second).of(30.seconds.from_now)


expect(json).to eq(
expect(json).to include(
'host_flg' => true, 'host_flg' => true,
'post_id' => nil, 'post_id' => nil,
'post_started_at' => nil 'post_started_at' => nil
) )

expect(json.fetch('watching_users')).to contain_exactly(
{
'id' => member.id,
'name' => 'member user'
}
)
end end
end end


@@ -167,11 +174,22 @@ RSpec.describe 'Theatres API', type: :request do
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
expect(theatre.reload.host_user_id).to eq(other_user.id) expect(theatre.reload.host_user_id).to eq(other_user.id)


expect(json).to eq(
expect(json).to include(
'host_flg' => false, 'host_flg' => false,
'post_id' => nil, 'post_id' => nil,
'post_started_at' => nil 'post_started_at' => nil
) )

expect(json.fetch('watching_users')).to contain_exactly(
{
'id' => member.id,
'name' => 'member user'
},
{
'id' => other_user.id,
'name' => 'other user'
}
)
end end
end end




+ 33
- 33
frontend/src/pages/theatres/TheatreDetailPage.tsx View File

@@ -2,6 +2,7 @@ import { useEffect, useRef, 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 ErrorScreen from '@/components/ErrorScreen'
import PostEmbed from '@/components/PostEmbed' import PostEmbed from '@/components/PostEmbed'
import PrefetchLink from '@/components/PrefetchLink' import PrefetchLink from '@/components/PrefetchLink'
import TagDetailSidebar from '@/components/TagDetailSidebar' import TagDetailSidebar from '@/components/TagDetailSidebar'
@@ -46,6 +47,7 @@ export default (() => {
const [content, setContent] = useState ('') const [content, setContent] = useState ('')
const [loading, setLoading] = useState (false) const [loading, setLoading] = useState (false)
const [sending, setSending] = useState (false) const [sending, setSending] = useState (false)
const [status, setStatus] = useState (200)
const [theatre, setTheatre] = useState<Theatre | null> (null) const [theatre, setTheatre] = useState<Theatre | null> (null)
const [theatreInfo, setTheatreInfo] = useState<TheatreInfo> (INITIAL_THEATRE_INFO) const [theatreInfo, setTheatreInfo] = useState<TheatreInfo> (INITIAL_THEATRE_INFO)
const [post, setPost] = useState<Post | null> (null) const [post, setPost] = useState<Post | null> (null)
@@ -60,7 +62,7 @@ export default (() => {
}, [videoLength]) }, [videoLength])


useEffect (() => { useEffect (() => {
lastCommentNoRef.current = comments.at (-1)?.no ?? 0
lastCommentNoRef.current = comments[0]?.no ?? 0
}, [comments]) }, [comments])


useEffect (() => { useEffect (() => {
@@ -85,7 +87,7 @@ export default (() => {
} }
catch (error) catch (error)
{ {
console.error (error)
setStatus ((error as any)?.response.status ?? 200)
} }
}) () }) ()


@@ -94,12 +96,6 @@ export default (() => {
} }
}, [id]) }, [id])


useEffect (() => {
commentsRef.current?.scrollTo ({
top: commentsRef.current.scrollHeight,
behavior: 'smooth' })
}, [comments])

useEffect (() => { useEffect (() => {
if (!(id)) if (!(id))
return return
@@ -122,7 +118,7 @@ export default (() => {
if (!(cancelled) && newComments.length > 0) if (!(cancelled) && newComments.length > 0)
{ {
lastCommentNoRef.current = newComments[newComments.length - 1].no lastCommentNoRef.current = newComments[newComments.length - 1].no
setComments (prev => [...prev, ...newComments])
setComments (prev => [...newComments, ...prev])
} }


const currentInfo = theatreInfoRef.current const currentInfo = theatreInfoRef.current
@@ -232,6 +228,9 @@ export default (() => {
embedRef.current?.seek (targetTime) embedRef.current?.seek (targetTime)
} }


if (status >= 400)
return <ErrorScreen status={status}/>

return ( return (
<div className="md:flex md:flex-1"> <div className="md:flex md:flex-1">
<Helmet> <Helmet>
@@ -270,7 +269,7 @@ export default (() => {


<SidebarComponent> <SidebarComponent>
<form <form
className="w-full h-auto border border-black dark:border-white rounded"
className="w-auto h-auto border border-black dark:border-white rounded mx-2"
onSubmit={async e => { onSubmit={async e => {
e.preventDefault () e.preventDefault ()


@@ -282,28 +281,20 @@ export default (() => {
setSending (true) setSending (true)
await apiPost (`/theatres/${ id }/comments`, { content }) await apiPost (`/theatres/${ id }/comments`, { content })
setContent ('') setContent ('')
commentsRef.current?.scrollTo ({
top: commentsRef.current.scrollHeight,
behavior: 'smooth' })
commentsRef.current?.scrollTo ({ top: 0, behavior: 'smooth' })
} }
finally finally
{ {
setSending (false) setSending (false)
} }
}}> }}>
<div className="p-2">
現在の同接数:{theatreInfo.watchingUsers.length}
</div>

<div className="overflow-x-hidden overflow-y-scroll text-wrap w-full h-32
border rounded">
<ul className="list-inside list-disc">
{theatreInfo.watchingUsers.map (user => (
<li key={user.id} className="px-4 py-1 text-sm">
{user.name || `名もなきニジラー(#${ user.id })`}
</li>))}
</ul>
</div>
<input
className="w-full p-2 border rounded"
type="text"
placeholder="ここにコメントを入力"
value={content}
onChange={e => setContent (e.target.value)}
disabled={sending}/>


<div <div
ref={commentsRef} ref={commentsRef}
@@ -324,14 +315,23 @@ export default (() => {
</div> </div>
</div>))} </div>))}
</div> </div>

<input
className="w-full p-2 border rounded"
type="text"
value={content}
onChange={e => setContent (e.target.value)}
disabled={sending}/>
</form> </form>

<div className="w-auto h-auto border border-black dark:border-white rounded mx-2 mt-4">
<div className="p-2">
現在の同接数:{theatreInfo.watchingUsers.length}
</div>

<div className="overflow-x-hidden overflow-y-scroll text-wrap w-full h-32
border rounded">
<ul className="list-inside list-disc">
{theatreInfo.watchingUsers.map (user => (
<li key={user.id} className="px-4 py-1 text-sm">
{user.name || `名もなきニジラー(#${ user.id })`}
</li>))}
</ul>
</div>
</div>
</SidebarComponent> </SidebarComponent>


<div className="md:hidden"> <div className="md:hidden">


Loading…
Cancel
Save