Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
118 commits
Select commit Hold shift + click to select a range
995e97c
feat: add comprehensive chat moderation tools for streamers
Josue19-08 Mar 25, 2026
ddd4e6e
chore: fix code formatting for CI
Josue19-08 Mar 25, 2026
c78b481
fix: add required curly braces for if statements
Josue19-08 Mar 25, 2026
90ceb73
test: update chat API tests for moderation enforcement
Josue19-08 Mar 25, 2026
466c282
test: fix mock count for chat moderation
Josue19-08 Mar 25, 2026
204a933
feat: add realtime notifications system
Depo-dev Mar 25, 2026
5450635
db: add stream access control foundation schema and migration
KevinMB0220 Mar 25, 2026
298da3b
lib: implement checkStreamAccess and API endpoint
KevinMB0220 Mar 25, 2026
3928f6e
api: update user and stream routes with access control fields
KevinMB0220 Mar 25, 2026
a707e94
feat: implement AccessGate component and frontend gating
KevinMB0220 Mar 25, 2026
b4a7ad8
feat: add Stream Access settings to creator dashboard
KevinMB0220 Mar 25, 2026
9899ae1
fix: normalize notification formatting for ci
Depo-dev Mar 25, 2026
d1ee166
feat: add quick-tip buttons and tip alerts in stream chat
Nwanne-san Mar 26, 2026
57130d8
feat: add paid access streams via one-time usdc payment
Nwanne-san Mar 26, 2026
d4c782d
feat: complete transak on-ramp integration (#364)
akintewe Mar 26, 2026
631d6c4
feat: token-gated streams via Stellar asset verification (#376)
akintewe Mar 26, 2026
987ae7c
Merge pull request #379 from Josue19-08/issue-370-chat-moderation
davedumto Mar 27, 2026
8840653
merge: resolve conflicts with origin/dev
KevinMB0220 Mar 27, 2026
78aba8a
Merge pull request #381 from KevinMB0220/feat/access-control-foundation
davedumto Mar 27, 2026
8829991
fix(stream-manager): preserve access config when saving stream info
KevinMB0220 Mar 27, 2026
38d1ce4
test(import): add 28 unit tests for import api route
KevinMB0220 Mar 27, 2026
fc14526
fix(lint): resolve pre-existing eslint errors blocking ci
KevinMB0220 Mar 27, 2026
50b7910
fix(security): single-pass html entity decode; fix remaining lint errors
KevinMB0220 Mar 27, 2026
54badeb
style: format streamaccesssettings and view-stream with prettier
KevinMB0220 Mar 27, 2026
913c8f2
Merge pull request #426 from KevinMB0220/feat/import-api-397
davedumto Mar 27, 2026
e1e2f69
merge: resolve conflict with upstream/dev in stream access check route
Depo-dev Mar 27, 2026
4b42b8d
Merge pull request #380 from Depo-dev/feat/realtime-notifications-system
davedumto Mar 27, 2026
c3e4697
feat(routes-f): add validation middleware, username conflict resoluti…
Josue19-08 Mar 27, 2026
a5cdc72
feat(routes-f): refactor import route to use shared validators and up…
Josue19-08 Mar 27, 2026
2e1ea13
feat(routes-f): add creator engagement endpoints
Ejere Mar 27, 2026
c584d6f
feat(routes-f): add notification settings endpoint
Ejere Mar 27, 2026
d8506f6
chore: apply prettier formatting to routes-f files
Josue19-08 Mar 27, 2026
e2ec08b
feat(deps): add @aws-sdk/client-s3 and @aws-sdk/s3-request-presigner
KevinMB0220 Mar 27, 2026
11d771e
fix(conflicts): replace Math.random with crypto.randomInt for secure …
Josue19-08 Mar 27, 2026
4f78eba
feat(uploads): add pre-signed upload url route
KevinMB0220 Mar 27, 2026
a9610d6
test(uploads): add 21 unit tests for upload sign route
KevinMB0220 Mar 27, 2026
32a6e0d
Merge pull request #470 from Josue19-08/feat/routes-f-validation-conf…
davedumto Mar 27, 2026
6e8c1f5
style: format uploads/sign route and tests with prettier
KevinMB0220 Mar 27, 2026
31d7dd7
Merge pull request #472 from KevinMB0220/feat/upload-sign-api-392
davedumto Mar 27, 2026
ed2842b
feat(routes-f): add viewer stream report submission endpoint
Themancalledpg Mar 27, 2026
5844928
Merge pull request #476 from Themancalledpg/feat/viewer-stream-report…
davedumto Mar 27, 2026
48c9b8b
feat(routes-f): implement discovery, subscriptions, comments, and ann…
walterthesmart Mar 27, 2026
3a76c5f
fix(discovery): resolve variable scoping and typing issues in discove…
walterthesmart Mar 27, 2026
1c80418
feat: add live viewer presence API with heartbeat and privacy opt-out
Just-Bamford Mar 27, 2026
3a97ebb
feat: add live chat history API with cursor pagination and moderator …
Just-Bamford Mar 27, 2026
2adaabd
feat: add live stream events API with internal secret auth and type f…
Just-Bamford Mar 27, 2026
84b1e57
feat: add stream settings API with validation and session auth
Just-Bamford Mar 27, 2026
f510c11
Merge pull request #478 from walterthesmart/feat/routes-f-enhancements
davedumto Mar 27, 2026
98499de
Merge pull request #481 from Just-Bamford/feat/stream-settings-api
davedumto Mar 28, 2026
eedf818
Merge pull request #477 from Just-Bamford/feat/live-viewer-presence-api
davedumto Mar 28, 2026
846605e
Merge pull request #480 from Just-Bamford/feat/live-stream-events-api
davedumto Mar 28, 2026
b6e13bf
Merge pull request #479 from Just-Bamford/feat/live-chat-history-api
davedumto Mar 28, 2026
7782223
Merge pull request #471 from Ejere/creator-engagement-routes
davedumto Mar 28, 2026
50d99ff
feat: add routes-f alerts, milestones, reactions, and collections
Chucks1093 Mar 28, 2026
db86766
Merge pull request #487 from Chucks1093/feat/routes-f-stellar-wave-45…
davedumto Mar 28, 2026
58be048
feat: add routes-f creator utility endpoints
HasToBeJames Mar 28, 2026
054ef49
feat: add watch page OG metadata, opengraph image, category canonical…
ogazboiz Mar 28, 2026
9725b5b
feat(routes-f): add creator leaderboard API
ogazboiz Mar 28, 2026
ad0b447
feat(routes-f): add trending clips endpoint
ogazboiz Mar 28, 2026
eaad2b1
feat(routes-f): add i18n translation string management endpoint
ogazboiz Mar 28, 2026
35611e8
Merge pull request #509 from playground-ogazboiz/feat/issue-442-i18n-…
davedumto Mar 28, 2026
d56e59d
Merge pull request #508 from playground-ogazboiz/feat/issue-435-trend…
davedumto Mar 28, 2026
ad2b1ef
Merge pull request #507 from playground-ogazboiz/feat/issue-391-leade…
davedumto Mar 28, 2026
5b40d19
Merge pull request #506 from playground-ogazboiz/feat/issue-366-watch…
davedumto Mar 28, 2026
3d6e6b0
fix: resolve ESLint errors blocking CI in transak integration
akintewe Mar 28, 2026
b50b418
Merge pull request #505 from HasToBeJames/codex/427-429-431-444-route…
davedumto Mar 28, 2026
2eb960f
chore: merge upstream/dev into feat/token-gated-streams, resolve conf…
akintewe Mar 28, 2026
96b1691
feat(routes-f): add devices drops recap and chapters endpoints
CMI-James Mar 28, 2026
9c25932
Merge pull request #510 from StreamFi-x/codex/feat-routes-f-all-482-4…
davedumto Mar 28, 2026
88d403c
feat(routes-f): user block and mute management endpoint
overprodigy Mar 28, 2026
4c47b77
feat(routes-f): meta api for platform stats, version, and features
overprodigy Mar 28, 2026
7e07020
feat(routes-f): dependency and third-party service health api
overprodigy Mar 28, 2026
752afa0
feat(routes-f): rate limit status endpoint for current user
overprodigy Mar 28, 2026
3b3369b
Merge pull request #511 from overprodigy/drips-contributions
davedumto Mar 28, 2026
65da184
feat(routes-f): sessions, clip submissions, stream rerun & polls endp…
JamesVictor-O Mar 28, 2026
2420790
Merge pull request #512 from JamesVictor-O/feat/routes-f-sessions-cli…
davedumto Mar 28, 2026
cf6f744
implement accessibility route
OnyemaAnthony Mar 28, 2026
44a9dfc
Implement app/api/routes-f/privacy/route.ts for managing per-user pri…
OnyemaAnthony Mar 28, 2026
8e1af07
implement creator dashboard summary
OnyemaAnthony Mar 28, 2026
df40708
implement creators collabration api
OnyemaAnthony Mar 28, 2026
a9acd38
feat(routes-f): implement creator team, loyalty tiers, and stream goa…
Themancalledpg Mar 28, 2026
6653019
Merge pull request #516 from OnyemaAnthony/creator-collaboration
davedumto Mar 28, 2026
5ee6956
Merge pull request #515 from OnyemaAnthony/creator-dashbard
davedumto Mar 28, 2026
4f8dbba
Merge pull request #514 from OnyemaAnthony/privacy-settings
davedumto Mar 28, 2026
27d409d
Merge branch 'dev' into accessibility-user
davedumto Mar 28, 2026
e440c1d
Merge pull request #513 from OnyemaAnthony/accessibility-user
davedumto Mar 28, 2026
f7a7d2d
Merge pull request #517 from Themancalledpg/feat/routes-f-creator-eng…
davedumto Mar 28, 2026
03b392f
feat(routes-f): add vod chapters list and create routes
KevinMB0220 Mar 29, 2026
0f9fb95
feat(routes-f): add vod chapter delete route
KevinMB0220 Mar 29, 2026
1a98106
feat(routes-f): add vod chapters bulk import route
KevinMB0220 Mar 29, 2026
a8243a1
test(routes-f): add unit tests for vod chapters routes
KevinMB0220 Mar 29, 2026
644a00d
Merge pull request #518 from KevinMB0220/feat/vod-chapters-491
davedumto Mar 29, 2026
0a0fd6b
feat(routes-f): add feedback, maintenance, export, and audit log APIs
Fran19-09 Mar 29, 2026
7257c80
Merge pull request #520 from Fran19-09/feat/routes-f-feedback-mainten…
davedumto Mar 29, 2026
4350c1a
Merge pull request #385 from akintewe/feat/token-gated-streams
davedumto Mar 29, 2026
5be92a7
Merge pull request #384 from akintewe/feat/transak-onramp-integration
davedumto Mar 29, 2026
2b6ed8c
feat(api): add monetization, integrations, content rules, and credits…
Hahfyeex Mar 29, 2026
f86378f
feat(routes-f): add engagement and recommendation apis
Akpamgbo Mar 29, 2026
e9cd962
Merge pull request #540 from Hahfyeex/feat/monetization-integrations-…
davedumto Mar 29, 2026
0a3f87b
feat(routes-f): creator bio management endpoint
OsagieCynthia Mar 29, 2026
fb99623
feat(routes-f): VOD recording management endpoint
OsagieCynthia Mar 29, 2026
fc010d6
feat(routes-f): streamer-safe background music integration endpoint
OsagieCynthia Mar 29, 2026
4e88fa5
feat(routes-f): clip subtitle and caption file management endpoint
OsagieCynthia Mar 29, 2026
3cfdc34
Merge pull request #542 from OsagieCynthia/feat/routes-f-multi-endpoints
davedumto Mar 29, 2026
89afb17
Merge pull request #541 from Akpamgbo/codex/410-413-417-483-routes-f-dev
davedumto Mar 29, 2026
341c6cd
fix: unify paid access, token gate, quick tips, and transak add-funds
Nwanne-san Mar 29, 2026
3981fc0
chore: merge feat-375 into feat-368 for unified stream resolution
Nwanne-san Mar 29, 2026
dc5fa7b
Merge branch 'dev' into feat-368-live-stream-tipping-chat
Nwanne-san Mar 29, 2026
d7e2122
Merge pull request #382 from Nwanne-san/feat-368-live-stream-tipping-…
davedumto Mar 29, 2026
37297ae
implemented frontend and dispute resolution documentation
Wilfred007 Mar 30, 2026
c07517b
fixes
Wilfred007 Mar 30, 2026
250074c
fixes
Wilfred007 Mar 30, 2026
f1f176a
Merge pull request #547 from Open-Works-Contributions/feat/routes
davedumto Mar 30, 2026
cd12704
feat(routes-f): add activity_events table schema and index
Sage-senpai Mar 30, 2026
0281387
feat(routes-f): add activity feed types
Sage-senpai Mar 30, 2026
3c74511
feat(routes-f): implement GET activity feed with cursor pagination an…
Sage-senpai Mar 30, 2026
f691428
feat(routes-f): implement GET activity/daily aggregated summary
Sage-senpai Mar 30, 2026
31fd9ce
feat(routes-f): insert activity events from follow, stream, and gift …
Sage-senpai Mar 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions app/[username]/watch/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { Metadata } from "next";
import { sql } from "@vercel/postgres";
import React from "react";

const BASE = process.env.NEXT_PUBLIC_APP_URL || "https://streamfi.com";

export async function generateMetadata({
params,
}: {
params: Promise<{ username: string }>;
}): Promise<Metadata> {
const { username } = await params;

let streamTitle = `${username} is live`;
let playbackId: string | null = null;

try {
const { rows } = await sql`
SELECT mux_playback_id, stream_title
FROM users
WHERE username = ${username}
LIMIT 1
`;
if (rows[0]) {
streamTitle = rows[0].stream_title || streamTitle;
playbackId = rows[0].mux_playback_id;
}
} catch {
// fallback to defaults
}

const title = `${streamTitle} — ${username} is live on StreamFi`;
const canonical = `${BASE}/${username}/watch`;
const ogImage = playbackId
? `https://image.mux.com/${playbackId}/thumbnail.jpg`
: `${BASE}/Images/streamFi.png`;

return {
title,
description: `Watch ${username} stream live on StreamFi`,
alternates: {
canonical,
},
openGraph: {
title,
description: `Watch ${username} stream live on StreamFi`,
url: canonical,
images: [{ url: ogImage, width: 1280, height: 720, alt: title }],
type: "website",
},
twitter: {
card: "summary_large_image",
title,
description: `Watch ${username} stream live on StreamFi`,
images: [ogImage],
},
};
}

export default function WatchLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}
158 changes: 158 additions & 0 deletions app/[username]/watch/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import { ImageResponse } from "next/og";
import { sql } from "@vercel/postgres";

export const runtime = "edge";
export const alt = "Live stream on StreamFi";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";

export default async function Image({
params,
}: {
params: Promise<{ username: string }>;
}) {
const { username } = await params;

let playbackId: string | null = null;
let avatar: string | null = null;
let streamTitle = `${username} is live`;

try {
const { rows } = await sql`
SELECT avatar, mux_playback_id, stream_title
FROM users
WHERE username = ${username}
LIMIT 1
`;
if (rows[0]) {
avatar = rows[0].avatar;
playbackId = rows[0].mux_playback_id;
streamTitle = rows[0].stream_title || streamTitle;
}
} catch {
// use defaults
}

const bgImage = playbackId
? `https://image.mux.com/${playbackId}/thumbnail.jpg`
: null;

return new ImageResponse(
(
<div
style={{
position: "relative",
width: "100%",
height: "100%",
display: "flex",
backgroundColor: "#0f0f0f",
}}
>
{bgImage && (
// eslint-disable-next-line @next/next/no-img-element
<img
src={bgImage}
alt=""
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
objectFit: "cover",
opacity: 0.6,
}}
/>
)}

{/* LIVE badge — top-left */}
<div
style={{
position: "absolute",
top: 32,
left: 32,
backgroundColor: "#e53e3e",
color: "white",
fontSize: 28,
fontWeight: 700,
padding: "8px 20px",
borderRadius: 8,
display: "flex",
}}
>
LIVE
</div>

{/* StreamFi wordmark — top-right */}
<div
style={{
position: "absolute",
top: 32,
right: 32,
color: "white",
fontSize: 28,
fontWeight: 700,
display: "flex",
}}
>
StreamFi
</div>

{/* Bottom gradient overlay with streamer info */}
<div
style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
padding: "60px 32px 44px",
background:
"linear-gradient(to top, rgba(0,0,0,0.85) 0%, transparent 100%)",
display: "flex",
flexDirection: "column",
gap: 10,
}}
>
{/* Avatar + username row */}
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
{avatar && (
// eslint-disable-next-line @next/next/no-img-element
<img
src={avatar}
alt=""
style={{
width: 56,
height: 56,
borderRadius: "50%",
objectFit: "cover",
}}
/>
)}
<span style={{ color: "white", fontSize: 32, fontWeight: 600 }}>
{username}
</span>
</div>

{/* Stream title */}
<span style={{ color: "#e2e8f0", fontSize: 24, fontWeight: 400 }}>
{streamTitle}
</span>
</div>

{/* Purple bottom accent line */}
<div
style={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
height: 6,
backgroundColor: "#7c3aed",
display: "flex",
}}
/>
</div>
),
{ ...size }
);
}
62 changes: 61 additions & 1 deletion app/[username]/watch/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"use client";

import { use, useEffect, useState, useRef } from "react";
import { use, useEffect, useState, useRef, useCallback } from "react";
import { notFound } from "next/navigation";
import ViewStream from "@/components/stream/view-stream";
import { ViewStreamSkeleton } from "@/components/skeletons/ViewStreamSkeleton";
import { AccessGate } from "@/components/stream/AccessGate";
import { toast } from "sonner";

interface PageProps {
Expand All @@ -23,6 +24,12 @@ interface UserData {
follower_count: number;
is_following: boolean;
stellar_address: string | null;
stream_access_type?: string;
stream_access_config?: {
asset_code: string;
asset_issuer: string;
min_balance: string;
} | null;
}

const WatchPage = ({ params }: PageProps) => {
Expand All @@ -34,6 +41,10 @@ const WatchPage = ({ params }: PageProps) => {
const [followLoading, setFollowLoading] = useState(false);
const [loggedInUsername, setLoggedInUsername] = useState<string | null>(null);

// Access control state
const [accessAllowed, setAccessAllowed] = useState<boolean | null>(null);
const [accessChecking, setAccessChecking] = useState(false);

// Viewer tracking: one unique ID per page visit
const viewerSessionId = useRef<string | null>(null);
const viewerPlaybackId = useRef<string | null>(null);
Expand All @@ -43,6 +54,35 @@ const WatchPage = ({ params }: PageProps) => {
setLoggedInUsername(sessionStorage.getItem("username"));
}, []);

const checkAccess = useCallback(async () => {
if (!userData) return;
// Public streams are always allowed — skip the network call
if (!userData.stream_access_type || userData.stream_access_type === "public") {
setAccessAllowed(true);
return;
}
setAccessChecking(true);
try {
const res = await fetch("/api/streams/access/check", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ streamerUsername: username }),
});
const data = await res.json();
setAccessAllowed(data.allowed === true);
} catch {
// On network failure, fail open rather than lock everyone out
setAccessAllowed(true);
} finally {
setAccessChecking(false);
}
}, [userData, username]);

useEffect(() => {
checkAccess();
}, [checkAccess]);

// Poll user/stream data every 5s
useEffect(() => {
let isInitialLoad = true;
Expand Down Expand Up @@ -201,6 +241,26 @@ const WatchPage = ({ params }: PageProps) => {
return notFound();
}

// Still waiting for access check result
if (accessAllowed === null || (accessChecking && accessAllowed === null)) {
return <ViewStreamSkeleton />;
}

// Access denied — show gate
if (!accessAllowed && userData.stream_access_config) {
return (
<div className="flex items-center justify-center min-h-screen p-6 bg-secondary">
<div className="w-full max-w-md">
<AccessGate
isLoading={accessChecking}
allowed={accessAllowed ?? false}
onRetry={checkAccess}
/>
</div>
</div>
);
}

const isOwner = loggedInUsername?.toLowerCase() === username.toLowerCase();

const transformedUserData = {
Expand Down
59 changes: 59 additions & 0 deletions app/api/debug/migrate-access-control/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { NextResponse } from "next/server";
import { sql } from "@vercel/postgres";

export async function GET() {
try {
const actions: string[] = [];

// --- stream_access_config (1 row per streamer) ---
await sql`
CREATE TABLE IF NOT EXISTS stream_access_config (
streamer_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
access_type VARCHAR(32) NOT NULL DEFAULT 'public',
config JSONB NOT NULL DEFAULT '{}'::jsonb,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
)
`;
actions.push("✅ Ensured table: stream_access_config");

// --- stream_access_grants (viewer grants, includes replay protection) ---
await sql`
CREATE TABLE IF NOT EXISTS stream_access_grants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
streamer_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
viewer_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
access_type VARCHAR(32) NOT NULL,
tx_hash VARCHAR(128),
amount_usdc VARCHAR(32),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE (streamer_id, viewer_id, access_type),
UNIQUE (tx_hash)
)
`;
actions.push("✅ Ensured table: stream_access_grants");

await sql`ALTER TABLE stream_access_grants ADD COLUMN IF NOT EXISTS amount_usdc VARCHAR(32)`;
actions.push("✅ Ensured column: stream_access_grants.amount_usdc");

// Indexes for fast access checks + dashboards
await sql`CREATE INDEX IF NOT EXISTS idx_stream_access_grants_streamer ON stream_access_grants(streamer_id)`;
actions.push("✅ Ensured index: idx_stream_access_grants_streamer");

await sql`CREATE INDEX IF NOT EXISTS idx_stream_access_grants_viewer ON stream_access_grants(viewer_id)`;
actions.push("✅ Ensured index: idx_stream_access_grants_viewer");

await sql`CREATE INDEX IF NOT EXISTS idx_stream_access_grants_type ON stream_access_grants(access_type)`;
actions.push("✅ Ensured index: idx_stream_access_grants_type");

await sql`CREATE INDEX IF NOT EXISTS idx_stream_access_config_type ON stream_access_config(access_type)`;
actions.push("✅ Ensured index: idx_stream_access_config_type");

return NextResponse.json({ ok: true, actions });
} catch (e) {
return NextResponse.json(
{ ok: false, error: e instanceof Error ? e.message : String(e) },
{ status: 500 }
);
}
}

8 changes: 7 additions & 1 deletion app/api/fetch-username/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ export async function GET(req: Request) {
try {
const result = wallet
? await sql`SELECT username FROM users WHERE wallet = ${wallet}`
: await sql`SELECT username FROM users WHERE email = ${email}`;
: await sql`
SELECT u.username
FROM users u
LEFT JOIN user_privacy p ON u.id = p.user_id
WHERE u.email = ${email}
AND (p.searchable_by_email IS TRUE OR p.searchable_by_email IS NULL)
`;

if (result.rows.length === 0) {
return withCorsResponse({ error: "User not found" }, 404);
Expand Down
Empty file removed app/api/routes-f/.gitkeep
Empty file.
Loading