diff --git a/app/settings.tsx b/app/settings.tsx index 9057531..2951769 100644 --- a/app/settings.tsx +++ b/app/settings.tsx @@ -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, @@ -811,7 +812,21 @@ export default function Settings() { Open Source Credits - + {/* Contributors Section */} + + + CONTRIBUTORS + + + {/* Footer */} diff --git a/assets/contributors.json b/assets/contributors.json new file mode 100644 index 0000000..4de5e4d --- /dev/null +++ b/assets/contributors.json @@ -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" + } +] \ No newline at end of file diff --git a/components/ContributorsList.tsx b/components/ContributorsList.tsx new file mode 100644 index 0000000..ff7dbb8 --- /dev/null +++ b/components/ContributorsList.tsx @@ -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 = ({ 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 ( + item.login} + numColumns={4} + contentContainerStyle={styles.gridContainer} + renderItem={({ item }) => ( + openProfile(item.html_url)} + > + + + )} + /> + ); + } + + return ( + item.login} + contentContainerStyle={styles.listContainer} + renderItem={({ item }) => ( + openProfile(item.html_url)} + > + + @{item.login} + {item.contributions && ( + + {item.contributions} + + )} + + )} + /> + ); +}; + +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", + }, +}); \ No newline at end of file diff --git a/components/flows/onboarding-steps.tsx b/components/flows/onboarding-steps.tsx index dd23461..9a23630 100644 --- a/components/flows/onboarding-steps.tsx +++ b/components/flows/onboarding-steps.tsx @@ -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[] = [ { @@ -45,4 +46,14 @@ export const onboardingSteps: OnboardingStep[] = [ ), customContent: , }, + { + id: "4", + title: "Meet the Contributors", + description: + "Whisper is built by a community of privacy advocates and open-source enthusiasts.", + icon: null, + customContent: , + }, + + ]; diff --git a/manual-contributors.json b/manual-contributors.json new file mode 100644 index 0000000..ca343b2 --- /dev/null +++ b/manual-contributors.json @@ -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" + } + +] \ No newline at end of file diff --git a/package.json b/package.json index a076791..a0bbb6b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/generate-contributors.js b/scripts/generate-contributors.js new file mode 100644 index 0000000..0c943b7 --- /dev/null +++ b/scripts/generate-contributors.js @@ -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(); \ No newline at end of file