ππ» myapp56μμλ νμλΌμΈ κΈλ³΄κΈ°λ₯Ό Home / Local / Federated νμΌλ‘ λ³Ό μ μμ΅λλ€.
In myapp56, you can view the timeline under the Home, Local, and Federated tabs.
ππ» μ 체 μ½λλ κΉνλΈμμ νμΈ ν μ μμ΅λλ€.
You can find the full code on GitHub.
https://github.com/gideonslife01/flm-nextjs
π μ 체 νλ‘μ νΈ κ΅¬μ‘° / Overall Project Structure
myapp project/
βββ app/ (Next.js App Router)
β βββ .well-known/webfinger/route.ts -> webfinger
β βββ api/follow/route.ts -> Follow API(temporary)
β βββ api/announce/route.ts -> Boost(Announcement)
β βββ api/like/route.ts. -> Like API
β βββ api/posts/route.ts -> Writing API(deprecated)
β βββ api/timeline/route.ts -> Timeline API
β βββ api/followinglist/route.ts -> followinglist API
β βββ api/followerslist/route.ts -> followerslist API
β βββ api/v1/instance/route.ts -> auth
β βββ api/v1/apps/route.ts. -> auth
β βββ api/v1/accounts/verify_credentials/route.ts -> auth
β βββ api/v1/statuses/route.ts -> Write Post
β βββ api/v1/timelines/home/route.ts -> timeline
β βββ api/v1/media/route.ts -> Image upload
β βββ api/v1/media/[id]/route.ts -> Image Delete
β βββ api/v1/instance/rules/route.ts -> Feature not implemented.
β βββ api/v1/instance/domain_blocks/route.ts -> Feature not implemented.
β βββ api/v1/filters/route.ts -> Feature not implemented.
β βββ api/v1/lists/route.ts -> Feature not implemented.
β βββ api/v1/announcements/route.ts -> Feature not implemented.
β βββ api/v1/timelines/public/route.ts -> Feature not implemented.
β βββ oauth/authorize/route.ts -> auth
β βββ oauth/token/route.ts -> auth
β βββ oauth/login-and-authorize/route.ts -> Tursky,Pinafore login processing
β βββ oauth/revoke/route.ts -> Feature not implemented.
β βββ api/v1/search/home/route.ts -> search
β βββ api/v2/search/home/route.ts -> search
β βββ api/v2/notifications/policy/route.ts-> Feature not implemented.
β βββ api/v1/accounts/[id]/followers/route.ts -> followers
β βββ api/v1/accounts/[id]/following/route.ts -> following
β βββ api/v1/statuses/[id]/route.ts -> -> Post delete(pinafore)
β βββ api/auth/signup/route.ts -> Website Signup
β βββ api/auth/login/route.ts -> Website Login
β βββ api/auth/logout/route.ts -> Website Logout
β βββ api/auth/refresh/route.ts -> Refresh Token
β βββ api/cleanup-orphan/route.ts -> post,outbox clean up
β βββ api/auth/me/route.tsx -> Login Check
β βββ auth/signup/page.tsx -> Signup UI
β βββ auth/signup/page.tsx -> Login UI
β βββ users/[username]/
β β βββ statuses/[id]/route.ts -> Indivisual Post
β β βββ route.ts -> Acotr Information
β β βββ followers/route.ts -> Followers List
β β βββ following/route.ts -> Following List
β β βββ inbox/route.ts -> Inbox
β β βββ outbox/route.ts -> outbox
β βββ usersui/[username]/
β β βββ page.tsx -> Timeline UI(Server, 404)
β β βββ ClientPage.tsx. -> Timeline UI(Client,Call Active Themes)
β β βββ _components/themes/
β β βββ themeex/ThemeexTheme.tsx -> Example Theme
β β βββ pinafore/PinaforeTheme.tsx -> Theme 1
β β βββ pinafore/FollowersList.tsx -> Theme 1, FollowersList
β β βββ pinafore/FollowingList.tsx -> Theme 1, FollowingList
β β βββ pinafore/NotificationsList.tsx -> Theme 1, NotificationsList
β β βββ pinafore/TimelineList.tsx -> Theme 1, TimelineList
β β βββ mastodon/MastodonTheme.tsx -> Theme 2
β β βββ minimal/MinimaltTheme.tsx -> Theme 3
β βββ layout.tsx, page.tsx, globals.css
β βββ favicon.ico
βββ lib/
β βββ theme.tsx -> Theme Provider
β βββ watchThemes.ts -> Check real-time theme changes
β βββ auth.ts -> Authentication, User Management
β βββ ap.ts -> Follow,Undo,Create,Likes,Announce
β βββ visibility.ts -> Visibility(Public,Unlisted,Private,Direct)
β βββ db.ts -> DB connection
βββ data/
β βββ keys/userIDs/ -> private.pem, public.pem(New)
β βββ keys/ -> private.pem, public.pem(legacy)
βββ data.sqlite -> Database(1/3)
βββ data.sqlite-wal -> Database(2/3)
βββ data.sqlite-shm -> Database(3/3)
βββ Caddyfile -> https
βββ instrumentation.ts -> Background Server
βββ package.json
π νλ‘μ νΈ μμ / Project Start
π κΈ°λ₯μ€λͺ / Feature Description
βοΈ Home
— νμ΄μ§ μμ μκ° λ‘κ·ΈμΈ ν κ²½μ° public, unlisted, private λͺ¨λ λ³Ό μ μμ
If the page owner is logged in, they can view public, unlisted, and private content.
— λ΄κ° νλ‘μ° ν μ¬λ κΈμ λ³Ό μ μμ.(λ΄ μλ²λ‘ λ°°λ¬λ κΈ)
You can view posts from people you follow (posts delivered to your server).
— Unlistedλ Localκ³Ό Pederatedμ κ³΅κ° μλ¨
Unlisted items are not visible in the Local or Federated timelines.
βοΈ Local
— λ΄ μλ²μ PublicκΈμ μ λΆ λ³΄μ.
All public posts on my server are visible.
βοΈ Federated
— λ΄ μλ²μ μ°κ²°λ λ€λ₯Έ μλ²μ PublicκΈμ μ λΆ λ³΄μ.
All public posts from other servers connected to my server are visible.
βοΈ Private
— νμ΄μ§ μμ μλ₯Ό νλ‘μ° ν μ¬λλ§ λ³Ό μ μμ
Visible only to people who follow the page owner.
βοΈ Direct
— νμ΄μ§ μμ μ+λ©μ
λ μ¬λμ΄ λ³Ό μ μμ.
Visible to the page owner and mentioned individuals.
π μ½λμμ / Code Modification
π‘ μλ λΌμ°νΈλ ꡬνλμ§ μμ λ§μ€ν λ νΈν λΌμ°νΈλ‘ κ°μ₯ λμ€μ ꡬν μμ
The route below is a Mastodon-compatible route that has not yet been implemented and is scheduled for implementation last.
api/v1/instance/rules/route.ts
api/v1/instance/domain_blocks/route.ts
api/v1/filters/route.ts
api/v1/lists/route.ts
api/v1/announcements/route.ts
api/v1/timelines/public/route.ts
oauth/login-and-authorize/route.ts
api/v2/notifications/policy/route.ts
βοΈ APIμμ / API Modification
— app/api/timeline/route.ts – μ½λ μμ / Code Modification
... ...
export async function GET(req: Request) {
... ...
const type = searchParams.get('type') || 'home'; // β
myapp56 - add : home, local, federated
... ...
// β
myapp56 - add type: home, local, federated
let rawTimeline: any[] = [];
if (type === 'local') {
// - Local: My server posts( visibility == public )
rawTimeline = db.prepare(`
SELECT id, content, username, username as actor, created_at, visibility, 'Note' as type, 'mine' as source, id as original_id
FROM posts WHERE visibility='public'
ORDER BY created_at DESC LIMIT 80
`).all() as any[];
} else if (type === 'federated') {
// - Federated: posts + inbox_posts (visibivlity == public)
rawTimeline = db.prepare(`
SELECT id, content, username, username as actor, created_at, visibility, 'Note' as type, 'mine' as source, id as original_id
FROM posts WHERE visibility='public'
UNION ALL
SELECT id, content, username, actor, created_at, visibility, type, 'inbox' as source, original_id
FROM inbox_posts WHERE visibility='public'
ORDER BY created_at DESC LIMIT 80
`).all() as any[];
} else {
// - Home: λ΄ νμλΌμΈ/My timeline + inbox_posts + posts
rawTimeline = db.prepare(`
SELECT id, content, username, username as actor, created_at, visibility, 'Note' as type, 'mine' as source, id as original_id
FROM posts WHERE username =?
UNION ALL
SELECT id, content, username, actor, created_at, visibility, type, 'inbox' as source, original_id
FROM inbox_posts WHERE username =?
ORDER BY created_at DESC LIMIT 80
`).all(username, username) as any[];
}
... ...
}
βοΈ ν λ§ νμΌ μμ / Edit theme files
— app/usersui/[username]/_components/themes/pinafore/TimelineList.tsx
// β
myapp56 - Hom,Local,Federated ν μΆκ° / Add Hom, Local, Federated Tabs
type TabType = 'home' | 'local' | 'federated';
type Props = {
timeline: any[];
currentUser: string | null;
tab: TabType;
onTabChange: (t: TabType) => void;
onDelete: (id: string) => void;
onBoost: (p: any) => void;
onLike: (p: any) => void;
onShare: (p: any) => void;
};
export default function TimelineList({
timeline, currentUser, tab, onTabChange, onDelete, onBoost, onLike, onShare }: Props) {
const [showDropdown, setShowDropdown] = useState(false); // β
myapp56 - Hom,Local,Federated ν μΆκ° / Add Hom, Local, Federated Tabs
... ...
return (
<>
<style>{`
/* β
myapp56 - timeline source tabs */
.timeline-dropdown-wrap {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 16px;
border-bottom: 1px solid #e6ecf0;
background: white;
position: sticky;
top: 0;
z-index: 5;
}
.timeline-dropdown { position: relative; }
.dropdown-toggle {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 14px;
border: 1px solid #e6ecf0;
border-radius: 20px;
background: white;
cursor: pointer;
font-size: 14px;
font-weight: 600;
}
.dropdown-toggle:hover { background: #f3f4f6; }
.dropdown-arrow { font-size: 10px; color: #999; margin-left: 4px; }
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
margin-top: 8px;
width: 220px;
background: white;
border: 1px solid #e6ecf0;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
overflow: hidden;
z-index: 10;
}
.dropdown-menu button {
width: 100%;
display: flex;
gap: 12px;
align-items: center;
padding: 12px 14px;
border: none;
background: white;
text-align: left;
cursor: pointer;
}
.dropdown-menu button:hover { background: #f7f9fa; }
.dropdown-menu button.active { background: #fafafe; }
.dropdown-menu button span:first-child { font-size: 18px; }
.dropdown-menu button div { display: flex; flex-direction: column; }
.dropdown-menu button b { font-size: 14px; }
.dropdown-menu button small { font-size: 11px; color: #657786; }
.timeline-count { font-size: 12px; color: #999; }
`}</style>
{/* β
myapp56 - Hom,Local,Federated ν μΆκ° / Add Hom, Local, Federated Tabs */}
<div className="timeline-dropdown-wrap">
<div className="timeline-dropdown">
<button className="dropdown-toggle" onClick={()=>setShowDropdown(!showDropdown)}>
<span>{tab==='home'?'π ': tab==='local'?'π':'π'}</span>
<span>{tab}</span>
<span>{showDropdown?'β²':'βΌ'}</span>
</button>
{showDropdown && (
<div className="dropdown-menu">
<button className={tab==='home'?'active':''} onClick={()=>{onTabChange('home'); setShowDropdown(false);}}>π Home</button>
<button className={tab==='local'?'active':''} onClick={()=>{onTabChange('local'); setShowDropdown(false);}}>π Local</button>
<button className={tab==='federated'?'active':''} onClick={()=>{onTabChange('federated'); setShowDropdown(false);}}>π Federated</button>
</div>
)}
</div>
<div className="timeline-count">{timeline.length} toots</div>
</div>
{timeline.map((p: any) => (
<article key={`${p.source}-${p.id}`} className="pinafore-status">
<Avatar actor={p.actor} username={p.username} />
<div className="status-content">
<div className="status-header">
<b>{getDisplayName(p.actor)}</b>
<span className="status-time">{new Date(p.created_at).toLocaleString()}</span>
{p.isBoostedPost && <span className="boost-label">π {getDisplayName(p.actor)}</span>}
{currentUser && p.isMine && (
<button onClick={() => onDelete(p.id)} style={{ color: '#e0245e', marginLeft: 'auto' }}>
π Delete
</button>
)}
</div>
<div className="status-text" dangerouslySetInnerHTML={{ __html: p.content }} />
{p.media_attachments && p.media_attachments.length > 0 && (
<div className={`status-media ${p.media_attachments.length === 1 ? 'single' : ''}`}>
{p.media_attachments.map((m: any) => (
<img key={m.id} src={toAbsolute(m.preview_url || m.url)} alt="media" loading="lazy" />
))}
</div>
)}
<div className="status-actions">
<button onClick={() => onBoost(p)} className={p.isMyBoost ? 'boosted' : ''}>π {p.boostCount || ''}</button>
<button onClick={() => onLike(p)} className={p.isMyLike ? 'liked' : ''}>β {p.likeCount || ''}</button>
<button onClick={() => onShare(p)}>β</button>
<button>π¬</button>
</div>
</div>
</article>
))}
</>
);
}
— app/usersui/[username]/_components/themes/pinafore/PinaforeTheme.tsx
// β
myapp56 - Home, Local, Federated ν μ ν μν / Home, Local, Federated tab selection state
const [tab, setTab] = useState<'home'|'local'|'federated'>('home');
... ...
// β
myapp56
// - Home / Local / Federated ν λ‘μ§
// / Home / Local / Federated tab logic
useEffect(() => {
if (view !== 'timeline') return;
// νλ‘ν νμ΄μ§λ©΄ username κΈ°λ°, μλλ©΄ type κΈ°λ°
// If it's a profile page, use username-based; otherwise, use type-based.
const url = tab === 'home'
? `/api/timeline?username=${username}&type=home`
: `/api/timeline?type=${tab}`;
fetch(url, { credentials: 'include', cache: 'no-store' })
.then(r => r.json())
.then(data => setTimeline(data))
.catch(e => console.error('tab fetch fail', e));
}, [tab, username, view]);
return (
<> ... ...
{view === 'timeline' && (
<>
{/* β
myapp56 - ADD Dropdown(Home/Local/Federated Tabs ) */}
<TimelineList
timeline={timeline}
tab={tab}
onTabChange={setTab}
currentUser={currentUser}
onDelete={handleDelete}
onBoost={handleBoostClick}
onLike={handleLikeClick}
onShare={onShare}
/>
</>
)} {/* end view === 'timeline' */}
... ... </>
);
βοΈ lib/visibility.ts – visibility κ·μΉμ λ§κ² μμ / Modify to comply with visibility rules.
export function canSee(viewer: string | null, post: any): boolean {
const vis = (post.visibility || 'public') as Visibility;
if (vis === 'public') return true;
if (vis === 'unlisted') return true; // λꡬλ λ³Ό μ μμ (μ΅λͺ
ν¬ν¨) / Anyone can see (including anonymous)
if (!viewer) return false; // private, directλ λ‘κ·ΈμΈ νμ / Login required for private, direct
if (post.username === viewer) return true; // λ΄ κΈμ λ€ λ³΄μ / I can see all my posts
if (vis === 'private') {
return isFollower(viewer, post.username); // νλ‘μλ§ / Only followers can see
}
if (vis === 'direct') {
// λ©μ
λ μ¬λλ§ / Only mentioned users can see
const mentions = post.mentions || [];
return mentions.includes(viewer);
}
return false;
}
π ν μ€νΈ / Test
βοΈ Hom,Local,Federated



βοΈ λλ‘λ€μ΄ λ©λ΄ / Dropdown menu

μν볡μ 8μ₯ 32μ / John 8:32
“κ·Έλ¦¬κ³ λν¬λ μ§λ¦¬λ₯Ό μκ² λ κ²μ΄λ©°, μ§λ¦¬κ° λν¬λ₯Ό μμ λ‘κ² ν κ²μ΄λ€.”
“Then you will know the truth ,and the truth will set you free”