コミットを比較

..

6 コミット

作成者 SHA1 メッセージ 日付
みてるぞ c4196845e0 Merge remote-tracking branch 'origin/main' into feature/264 2026-04-11 22:12:49 +09:00
みてるぞ 72e3b10b3c Merge branch 'feature/264' of https://git.miteruzo.com/miteruzo/btrc-hub into feature/264 2026-04-11 22:11:21 +09:00
みてるぞ f51e06eaf9 #264 2026-04-11 22:09:22 +09:00
みてるぞ c36b2c8a1b 投稿に対する履歴(#264) (#307)
Merge branch 'main' into feature/264

#264

#264

#264

#264

Co-authored-by: miteruzo <miteruzo@naver.com>
Reviewed-on: #307
2026-04-11 17:05:57 +09:00
みてるぞ 0bc5d3135b Merge branch 'main' into feature/264 2026-04-11 17:05:46 +09:00
みてるぞ e021423904 スマホ・レーアウト・バグ(#304) (#305)
#304

#304

#304

Co-authored-by: miteruzo <miteruzo@naver.com>
Reviewed-on: #305
2026-04-11 16:58:41 +09:00
9個のファイルの変更215行の追加20行の削除
+65
ファイルの表示
@@ -145,5 +145,70 @@ RSpec.describe Tag, type: :model do
expect(target_tag.reload.post_count).to eq(0) expect(target_tag.reload.post_count).to eq(0)
end end
end end
def snapshot_tags(post)
post.snapshot_tag_names.join(' ')
end
def create_post_version_for!(post, version_no: 1, event_type: 'create', created_by_user: nil)
PostVersion.create!(
post: post,
version_no: version_no,
event_type: event_type,
title: post.title,
url: post.url,
thumbnail_base: post.thumbnail_base,
tags: snapshot_tags(post),
parent: post.parent,
original_created_from: post.original_created_from,
original_created_before: post.original_created_before,
created_at: Time.current,
created_by_user: created_by_user
)
end
context 'when post versions are enabled' do
let!(:source_post_tag) { PostTag.create!(post: post_record, tag: source_tag) }
let!(:unaffected_post) do
Post.create!(url: 'https://example.com/posts/2', title: 'unaffected post')
end
before do
create_post_version_for!(post_record)
create_post_version_for!(unaffected_post)
end
it 'creates an update post_version only for affected posts' do
expect {
described_class.merge_tags!(target_tag, [source_tag])
}.to change(PostVersion, :count).by(1)
affected_versions = post_record.reload.post_versions.order(:version_no)
expect(affected_versions.pluck(:version_no)).to eq([1, 2])
latest = affected_versions.last
expect(latest.event_type).to eq('update')
expect(latest.created_by_user).to be_nil
expect(latest.tags).to eq(snapshot_tags(post_record.reload))
expect(unaffected_post.reload.post_versions.count).to eq(1)
end
end
context 'when the source tag has no active post_tags' do
let!(:another_post) do
Post.create!(url: 'https://example.com/posts/3', title: 'another post')
end
before do
create_post_version_for!(another_post)
end
it 'does not create any post_version' do
expect {
described_class.merge_tags!(target_tag, [source_tag])
}.not_to change(PostVersion, :count)
end
end
end end
end end
+124
ファイルの表示
@@ -90,4 +90,128 @@ RSpec.describe "nico:sync" do
expect(active_names).to include("nico:NEW") expect(active_names).to include("nico:NEW")
expect(active_names).not_to include("nico:OLD") expect(active_names).not_to include("nico:OLD")
end end
def snapshot_tags(post)
post.snapshot_tag_names.join(' ')
end
def create_post_version_for!(post, version_no: 1, event_type: 'create', created_by_user: nil)
PostVersion.create!(
post: post,
version_no: version_no,
event_type: event_type,
title: post.title,
url: post.url,
thumbnail_base: post.thumbnail_base,
tags: snapshot_tags(post),
parent: post.parent,
original_created_from: post.original_created_from,
original_created_before: post.original_created_before,
created_at: Time.current,
created_by_user: created_by_user
)
end
it '新規 post 作成時に version 1 を作る' do
Tag.bot
Tag.tagme
Tag.niconico
Tag.video
Tag.no_deerjikist
stub_python([{
'code' => 'sm9',
'title' => 't',
'tags' => ['AAA'],
'uploaded_at' => '2026-01-01 12:34:56'
}])
allow(URI).to receive(:open).and_return(StringIO.new('<html></html>'))
expect {
run_rake_task('nico:sync')
}.to change(PostVersion, :count).by(1)
post = Post.find_by!(url: 'https://www.nicovideo.jp/watch/sm9')
version = post.post_versions.order(:version_no).last
expect(version.version_no).to eq(1)
expect(version.event_type).to eq('create')
expect(version.created_by_user).to be_nil
expect(version.tags).to eq(snapshot_tags(post.reload))
end
it '既存 post の内容または tags が変わったとき update version を作る' do
post = Post.create!(
title: 'old',
url: 'https://www.nicovideo.jp/watch/sm9',
uploaded_user: nil
)
kept_general = create_tag!('spec_kept', category: 'general')
PostTag.create!(post: post, tag: kept_general)
create_post_version_for!(post)
linked = create_tag!('spec_linked', category: 'general')
nico = create_tag!('nico:AAA', category: 'nico')
link_nico_to_tag!(nico, linked)
Tag.bot
Tag.tagme
Tag.no_deerjikist
stub_python([{
'code' => 'sm9',
'title' => 't',
'tags' => ['AAA'],
'uploaded_at' => '2026-01-01 12:34:56'
}])
allow(URI).to receive(:open).and_return(StringIO.new('<html></html>'))
expect {
run_rake_task('nico:sync')
}.to change(PostVersion, :count).by(1)
version = post.reload.post_versions.order(:version_no).last
expect(version.version_no).to eq(2)
expect(version.event_type).to eq('update')
expect(version.created_by_user).to be_nil
expect(version.tags).to eq(snapshot_tags(post.reload))
end
it '既存 post に差分が無いときは新しい version を作らない' do
nico = create_tag!('nico:AAA', category: 'nico')
no_deerjikist = create_tag!('ニジラー情報不詳', category: 'meta')
post = Post.create!(
title: 't',
url: 'https://www.nicovideo.jp/watch/sm9',
uploaded_user: nil,
original_created_from: Time.iso8601('2026-01-01T03:34:00Z'),
original_created_before: Time.iso8601('2026-01-01T03:35:00Z')
)
PostTag.create!(post: post, tag: nico)
PostTag.create!(post: post, tag: no_deerjikist)
create_post_version_for!(post)
stub_python([{
'code' => 'sm9',
'title' => 't',
'tags' => ['AAA'],
'uploaded_at' => '2026-01-01 12:34:56'
}])
allow(URI).to receive(:open).and_return(StringIO.new('<html></html>'))
expect {
run_rake_task('nico:sync')
}.not_to change(PostVersion, :count)
version = post.reload.post_versions.order(:version_no).last
expect(version.version_no).to eq(1)
expect(version.event_type).to eq('create')
expect(version.tags).to eq(snapshot_tags(post.reload))
end
end end
+1 -1
ファイルの表示
@@ -131,7 +131,7 @@ export default (() => {
<> <>
<RouteBlockerOverlay/> <RouteBlockerOverlay/>
<BrowserRouter> <BrowserRouter>
<div className="flex flex-col h-dvh w-screen"> <div className="flex flex-col h-dvh w-full">
<TopNav user={user}/> <TopNav user={user}/>
<RouteTransitionWrapper user={user} setUser={setUser}/> <RouteTransitionWrapper user={user} setUser={setUser}/>
</div> </div>
+11 -11
ファイルの表示
@@ -79,12 +79,12 @@ export default (({ user }: Props) => {
{ name: '上位タグ', to: '/tags/implications', visible: false }, { name: '上位タグ', to: '/tags/implications', visible: false },
{ name: 'ニコニコ連携', to: '/tags/nico' }, { name: 'ニコニコ連携', to: '/tags/nico' },
{ name: 'ヘルプ', to: '/wiki/ヘルプ:タグ' }] }, { name: 'ヘルプ', to: '/wiki/ヘルプ:タグ' }] },
{ name: '素材', to: '/materials', subMenu: [ // { name: '素材', to: '/materials', subMenu: [
{ name: '一覧', to: '/materials' }, // { name: '一覧', to: '/materials' },
// { name: '検索', to: '/materials/search' }, // { name: '検索', to: '/materials/search', visible: false },
{ name: '追加', to: '/materials/new' }, // { name: '追加', to: '/materials/new' },
// { name: '履歴', to: '/materials/changes' }, // { name: '履歴', to: '/materials/changes', visible: false },
{ name: 'ヘルプ', to: 'wiki/ヘルプ:素材集' }] }, // { name: 'ヘルプ', to: '/wiki/ヘルプ:素材集' }] },
{ name: '上映会', to: '/theatres/1', base: '/theatres', subMenu: [ { name: '上映会', to: '/theatres/1', base: '/theatres', subMenu: [
{ name: <>&thinsp;1&thinsp;</>, to: '/theatres/1' }, { name: <>&thinsp;1&thinsp;</>, to: '/theatres/1' },
{ name: 'CyTube', to: '//cytube.mm428.net/r/deernijika' }, { name: 'CyTube', to: '//cytube.mm428.net/r/deernijika' },
@@ -145,9 +145,9 @@ export default (({ user }: Props) => {
return ( return (
<> <>
<nav className="px-3 flex justify-between items-center w-full min-h-[48px] <nav className="px-3 flex justify-between items-center w-full
bg-yellow-200 dark:bg-red-975 md:bg-yellow-50"> bg-yellow-200 dark:bg-red-975 md:bg-yellow-50">
<div className="flex items-center gap-2 h-full"> <div className="flex items-center gap-2 h-12">
<PrefetchLink <PrefetchLink
to="/posts" to="/posts"
className="mx-4 text-xl font-bold text-pink-600 hover:text-pink-400 className="mx-4 text-xl font-bold text-pink-600 hover:text-pink-400
@@ -158,13 +158,13 @@ export default (({ user }: Props) => {
</PrefetchLink> </PrefetchLink>
<div ref={navRef} className="relative hidden md:flex h-full items-center"> <div ref={navRef} className="relative hidden md:flex h-12 items-center">
<div aria-hidden <div aria-hidden
className={cn ('absolute top-1/2 -translate-y-1/2 h-full', className={cn ('absolute inset-y-0 h-12',
'bg-yellow-200 dark:bg-red-950', 'bg-yellow-200 dark:bg-red-950',
'transition-[transform,width] duration-200 ease-out')} 'transition-[transform,width] duration-200 ease-out')}
style={{ width: hl.width, style={{ width: hl.width,
transform: `translate(${ hl.left }px, -50%)`, transform: `translateX(${ hl.left }px)`,
opacity: hl.visible ? 1 : 0 }}/> opacity: hl.visible ? 1 : 0 }}/>
{menu.map ((item, i) => ( {menu.map ((item, i) => (
+1 -2
ファイルの表示
@@ -8,8 +8,7 @@ type Props = {
export default (({ children, className }: Props) => ( export default (({ children, className }: Props) => (
<main className={cn ('flex-1 overflow-y-auto p-4', <main className={cn ('flex-1 overflow-y-auto p-4 md:h-[calc(100dvh-88px)]',
'md:h-[calc(100dvh-88px)] md:overflow-y-auto',
className)}> className)}>
{children} {children}
</main>)) satisfies FC<Props> </main>)) satisfies FC<Props>
+5 -3
ファイルの表示
@@ -46,10 +46,12 @@ a
body body
{ {
margin: 0; margin: 0;
display: flex;
place-items: center;
min-width: 320px; min-width: 320px;
min-height: 100vh; }
#root
{
min-height: 100dvh;
} }
h1 h1
+2 -1
ファイルの表示
@@ -6,7 +6,8 @@ import type { FC } from 'react'
export default (() => ( export default (() => (
<div className="md:flex md:flex-1 md:h-[calc(100dvh-88px)]"> <div className="md:flex md:flex-1 overflow-y-auto md:overflow-y-hidden
md:h-[calc(100dvh-88px)]">
<MaterialSidebar/> <MaterialSidebar/>
<Outlet/> <Outlet/>
</div>)) satisfies FC </div>)) satisfies FC
+2 -1
ファイルの表示
@@ -93,7 +93,8 @@ export default (({ user }: Props) => {
: 'bg-gray-500 hover:bg-gray-600') : 'bg-gray-500 hover:bg-gray-600')
return ( return (
<div className="md:flex md:flex-1 md:h-[calc(100dvh-88px)]"> <div className="md:flex md:flex-1 overflow-y-auto md:overflow-y-hidden
md:h-[calc(100dvh-88px)]">
<Helmet> <Helmet>
{(post?.thumbnail || post?.thumbnailBase) && ( {(post?.thumbnail || post?.thumbnailBase) && (
<meta name="thumbnail" content={post.thumbnail || post.thumbnailBase}/>)} <meta name="thumbnail" content={post.thumbnail || post.thumbnailBase}/>)}
+4 -1
ファイルの表示
@@ -69,7 +69,10 @@ export default (() => {
}, [location.search]) }, [location.search])
return ( return (
<div className="md:flex md:flex-1 md:h-[calc(100dvh-88px)]" ref={containerRef}> <div
className="md:flex md:flex-1 overflow-y-auto md:overflow-y-hidden
md:h-[calc(100dvh-88px)]"
ref={containerRef}>
<Helmet> <Helmet>
<title> <title>
{tags.length {tags.length