Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 16 additions & 1 deletion app/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ModeToggle } from "@/components/ui/mode-toggle";
import { Separator } from "@/components/ui/separator";
import { Text } from "@/components/ui/text";
import { View } from "@/components/ui/view";
import { ContributorsList } from "@/components/ContributorsList";
import {
type ExportFormat,
exportAllChats,
Expand Down Expand Up @@ -811,7 +812,21 @@ export default function Settings() {
Open Source Credits
</Button>
</View>

{/* Contributors Section */}
<View style={{ marginBottom: 8 }}>
<Text
variant="label"
style={{
fontSize: 13,
fontWeight: "600",
opacity: 0.7,
marginBottom: 12,
}}
>
CONTRIBUTORS
</Text>
<ContributorsList variant="settings" />
</View>
<Separator />

{/* Footer */}
Expand Down
34 changes: 34 additions & 0 deletions assets/contributors.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[
{
"id": 30320791,
"login": "mrspence",
"avatar_url": "https://avatars.githubusercontent.com/u/30320791?v=4",
"html_url": "https://github.com/mrspence"
},
{
"id": 89926355,
"login": "will-lamerton",
"avatar_url": "https://avatars.githubusercontent.com/u/89926355?v=4",
"html_url": "https://github.com/will-lamerton"
},
{
"id": 49699333,
"login": "dependabot[bot]",
"avatar_url": "https://avatars.githubusercontent.com/in/29110?v=4",
"html_url": "https://github.com/apps/dependabot"
},
{
"id": "manual-1",
"login": "marketing-guru",
"avatar_url": "https://avatars.githubusercontent.com/u/583231?v=4",
"html_url": "https://github.com/marketing-guru",
"type": "Marketing"
},
{
"id": "ronak-1",
"login": "RONAK-AI647",
"avatar_url": "https://avatars.githubusercontent.com/RONAK-AI647",
"html_url": "https://github.com/RONAK-AI647",
"type": "Developer"
}
]
117 changes: 117 additions & 0 deletions components/ContributorsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import type React from "react";
import {
FlatList,
Image,
Linking,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";

import contributorsData from "../assets/contributors.json";

interface ContributorsListProps {
variant: "onboarding" | "settings";
}

export const ContributorsList: React.FC<ContributorsListProps> = ({ variant }) => {
const openProfile = (url: string) => {
if (url) {
// biome-ignore lint/suspicious/noConsole: Needed for openURL catch logging
Linking.openURL(url).catch((err) => console.error("Couldn't load page", err));
}
};

if (variant === "onboarding") {
return (
<FlatList
data={contributorsData}
keyExtractor={(item) => item.login}
numColumns={4}
contentContainerStyle={styles.gridContainer}
renderItem={({ item }) => (
<TouchableOpacity
style={styles.avatarWrapper}
onPress={() => openProfile(item.html_url)}
>
<Image source={{ uri: item.avatar_url }} style={styles.gridAvatar} />
</TouchableOpacity>
)}
/>
);
}

return (
<FlatList
data={contributorsData}
keyExtractor={(item) => item.login}
contentContainerStyle={styles.listContainer}
renderItem={({ item }) => (
<TouchableOpacity
style={styles.rowContainer}
onPress={() => openProfile(item.html_url)}
>
<Image source={{ uri: item.avatar_url }} style={styles.listAvatar} />
<Text style={styles.username}>@{item.login}</Text>
{item.contributions && (
<View style={styles.badge}>
<Text style={styles.badgeText}>{item.contributions}</Text>
</View>
)}
</TouchableOpacity>
)}
/>
);
};

const styles = StyleSheet.create({
gridContainer: {
padding: 16,
marginTop: 40,
alignItems: "center",
},
avatarWrapper: {
margin: 8,
},
gridAvatar: {
width: 60,
height: 60,
borderRadius: 30,
backgroundColor: "#333",
},
listContainer: {
paddingVertical: 8,
},
rowContainer: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 12,
paddingHorizontal: 16,
borderBottomWidth: 0.5,
borderBottomColor: "#333",
},
listAvatar: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: "#333",
marginRight: 16,
},
username: {
flex: 1,
fontSize: 16,
fontWeight: "500",
color: "#fff",
},
badge: {
backgroundColor: "#222",
paddingHorizontal: 8,
paddingVertical: 3,
borderRadius: 10,
},
badgeText: {
fontSize: 12,
color: "#888",
},
});
11 changes: 11 additions & 0 deletions components/flows/onboarding-steps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Colors } from "@/theme/colors";
import { Bird, ShieldCheck } from "lucide-react-native";
import { Logo } from "../logo";
import { LocalAuthStepContent } from "./local-auth-step";
import { ContributorsList } from "@/components/ContributorsList";

export const onboardingSteps: OnboardingStep[] = [
{
Expand Down Expand Up @@ -45,4 +46,14 @@ export const onboardingSteps: OnboardingStep[] = [
),
customContent: <LocalAuthStepContent />,
},
{
id: "4",
title: "Meet the Contributors",
description:
"Whisper is built by a community of privacy advocates and open-source enthusiasts.",
icon: null,
customContent: <ContributorsList variant="onboarding" />,
},


];
17 changes: 17 additions & 0 deletions manual-contributors.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[
{
"id": "manual-1",
"login": "marketing-guru",
"avatar_url": "https://avatars.githubusercontent.com/u/583231?v=4",
"html_url": "https://github.com/marketing-guru",
"type": "Marketing"
},
{
"id": "ronak-1",
"login": "RONAK-AI647",
"avatar_url": "https://avatars.githubusercontent.com/RONAK-AI647",
"html_url": "https://github.com/RONAK-AI647",
"type": "Developer"
}

]
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"scripts": {
"start": "expo start",
"reset-project": "node ./scripts/reset-project.js",
"generate-contributors": "node scripts/generate-contributors.js",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web",
Expand Down
66 changes: 66 additions & 0 deletions scripts/generate-contributors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// scripts/generate-contributors.js
const fs = require('node:fs');
const path = require('node:path');

const GITHUB_API_URL = 'https://api.github.com/repos/Whisper-AI-App/app/contributors';
const MANUAL_FILE_PATH = path.join(__dirname, '../manual-contributors.json');
const OUTPUT_FILE_PATH = path.join(__dirname, '../assets/contributors.json');

async function generateContributors() {
try {
console.log('Fetching GitHub contributors...');

// 1. Fetch code contributors (Requires Node 18+ for native fetch)
const response = await fetch(GITHUB_API_URL, {
headers: {
'User-Agent': 'Whisper-AI-App-Build-Script'
}
});

if (!response.ok) throw new Error(`GitHub API responded with ${response.status}`);
const codeContributors = await response.json();

// 2. Read manual contributors
let manualContributors = [];
if (fs.existsSync(MANUAL_FILE_PATH)) {
const manualData = fs.readFileSync(MANUAL_FILE_PATH, 'utf-8');
manualContributors = JSON.parse(manualData);
}

// 3. GitHub contributors first (by commit count), manual appended after
const uniqueMap = new Map();

codeContributors.forEach(contributor => {
uniqueMap.set(contributor.login, {
id: contributor.id,
login: contributor.login,
avatar_url: contributor.avatar_url,
html_url: contributor.html_url,
});
});

manualContributors.forEach(contributor => {
if (!uniqueMap.has(contributor.login)) {
uniqueMap.set(contributor.login, {
id: contributor.id,
login: contributor.login,
avatar_url: contributor.avatar_url,
html_url: contributor.html_url,
type: contributor.type,
});
}
});

const finalContributorsList = Array.from(uniqueMap.values());

// 4. Save to assets folder
fs.writeFileSync(OUTPUT_FILE_PATH, JSON.stringify(finalContributorsList, null, 2));
console.log(`Successfully generated ${finalContributorsList.length} contributors to assets/contributors.json`);

} catch (error) {
console.error('Failed to generate contributors:', error.message);
process.exit(1);
}
}

generateContributors();
Loading