diff --git a/.gitignore b/.gitignore index c9d575d7..79c113f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,38 +1,45 @@ -# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files - -# dependencies -node_modules/ - -# Expo -.expo/ -dist/ -web-build/ -expo-env.d.ts - -# Native -*.orig.* -*.jks -*.p8 -*.p12 -*.key -*.mobileprovision - -# Metro -.metro-health-check* - -# debug -npm-debug.* -yarn-debug.* -yarn-error.* - -# macOS +# Miscellaneous +*.class +*.log +*.pyc +*.swp .DS_Store -*.pem - -# local env files -.env*.local - -# typescript -*.tsbuildinfo - -app-example +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/.metadata b/.metadata new file mode 100644 index 00000000..d77a4e0b --- /dev/null +++ b/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "c23637390482d4cf9598c3ce3f2be31aa7332daf" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + - platform: android + create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + - platform: ios + create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + - platform: linux + create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + - platform: macos + create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + - platform: web + create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + - platform: windows + create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 80fa91e9..00000000 --- a/.prettierrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "singleQuote": true, - "semi": false, - "printWidth": 130 -} \ No newline at end of file diff --git a/README.md b/README.md index bf30949c..57de8967 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,87 @@ -# front-end -The front end repository of Spark Social. +# TikTok Clone with Flutter + +A clean, beautiful and functional TikTok clone built with Flutter using Apple Cupertino design principles. + +## Features + +- **Home Feed:** Vertical scrolling video feed similar to TikTok +- **Search & Discover:** Explore content with trending hashtags and a discovery grid +- **Video Creation:** Record and publish videos with effects and filters +- **Messages:** Chat with other users +- **Profile:** View your own content, liked videos, and manage your profile +- **Clean UI:** Uses Cupertino design for a clean, iOS-like experience +- **Ionicons:** Beautiful icons throughout the app + +## Screenshots + +(Screenshots would be here) + +## Getting Started + +### Prerequisites + +- Flutter SDK (3.7.2 or higher) +- Dart SDK (3.0.0 or higher) +- iOS/Android development environment + +### Installation + +1. Clone this repository +```bash +git clone https://github.com/yourusername/tiktok-clone.git +``` + +2. Navigate to the project directory +```bash +cd tiktok-clone +``` + +3. Install dependencies +```bash +flutter pub get +``` + +4. Run the app +```bash +flutter run +``` + +## Technologies Used + +- Flutter +- Cupertino (iOS-style) widgets +- Provider for state management +- Ionicons for beautiful icons +- Video player for media playback +- Camera for video recording +- Animation for smooth transitions + +## Project Structure + +- `lib/screens` - Contains the main screens of the application +- `lib/widgets` - Reusable UI components +- `lib/models` - Data models +- `lib/services` - Business logic and services +- `lib/utils` - Utility functions and constants + +## Future Enhancements + +- User authentication +- Real-time video streaming +- Comments and direct messaging +- Content recommendation algorithm +- Filters and effects +- Sound library + +## Contributing + +Contributions are welcome! Feel free to submit a Pull Request. + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. + +## Acknowledgments + +- Inspired by TikTok's excellent user experience +- Built with Flutter, the beautiful UI framework diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 00000000..f9b30346 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 00000000..be3943c9 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 00000000..5c63124b --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.sparksocial" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.sparksocial" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..33796bc1 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/example/sparksocial/MainActivity.kt b/android/app/src/main/kotlin/com/example/sparksocial/MainActivity.kt new file mode 100644 index 00000000..23f787e4 --- /dev/null +++ b/android/app/src/main/kotlin/com/example/sparksocial/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.sparksocial + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..f74085f3 --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0y~yVDJE84rT@hh9qO>QU(SF$r9IylHmNblJdl&R0hYC z{G?O`&)mfH)S%SFl*+=BsWuD@4C?}XLR=Xb7#RJ!7{h04Zu=H;^aq4w44VZJaXaum zG@>0o`I5 z6U6gpN>(hBs@ovlyiK}omrVN}&28UY_JdsVcA=Cl0|SF_NswPKgMfg5KtMnO2sE4r zf%}pIPku2lFue41aSVw#{PxUsz9t6&hrp0GdlTMw^~h@f`fs1s!olsWbZ#C0v7PJ9 zExVjNeb(Ol`RU07XFf|X@qSK7qke0_C&kUH>MnD(8u-mi5B zt(&|xZ@%@sBNk%nt_5+E&U1QAlyT>>sMB2Zd5T5*R3&Sld5=98o%0qdm+@@-JjKB8 z(xcUiGOB?O=e)LCdt!o~wXBiQlFwyn4J{(JaZG%Q-e|yQz{EjrrIztFmwg@gt#&=FffMC4mtWmbKAF&qdyqKXG0ipk&vT5 zAmZ%d{SXF=bCKq@Z-GZZ;tV#~j83JDzFmxd-Hahq8AGNqhE8V;o5>h9i!po_WB6=l zU2kT?Kvt(bR^JA;;7+;DUGlvL6sMk6nsrHO&Q;BA-wby`jSBSdXJueu5G@Jv3uX`y zP*5;1FzBB@fBpXR=g+?ff&5MV!VC-yOFdm2Lp+YZy>e2h$w0vMVh8iNMNLhM)++IJ z{QqCg*LgRGXKpKZaOLSMJFo8MNc{figWv3mbJp+w-BoBm(|-hT|4dcbJ2lEyX!??Y`jpZ81*D{;rF6b`?;0>>pbJRKPRw2)?8wK8GH9> zL)n=k&vkA#ITl(SSsBW`??>AG{pM|AK2Jc#t6>y4N#$)H8ZEGkUc!`m{6pbTImMF#2{f z`gJk-_b~?aGX_m$44TXsJQ-wH$W+GA>5O4B7{g{VhRtFOpT!tHo5?tW$tsn}K99+{ zjLE&4*|Uz>tC88eNi3sZJbQ{n(LBkDd1t=Sn-plsE283-d*czTa-zm@iQD#kujt@|Kvt4hp|H z71QJA-@DE-vCc=}m3vdfeqD}l&W>B;IduNM%M_}RPx;yPVrk2E_T?&?d+vqXsA}xF zGk52_Ny78;-$vOoENNLjTll#WOG?YLuZ?eO;uVxk)@?h*_~6>qo(~l}_f@BaD>p=c zm>yaWRpaQm|`^KGX?!;c=*_;KycpYksb1 rdBk${xlomwC<})`ozHbP0l+XkKcl<5t literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0y~yV3+{H9Lx+13>Rhybuln7NS3%plmzFem6RtIr7}3C z$Dctjh(SM?K|h4SAdJB{hQT_6!9JhSFr3jep3y9U z(L9OKDxJ|flhG!d(I$t{HiywRm(ebd(Y}z;v6#`Zgwd&#(Yb=rrHawDn$fL>(XE!z zt&Y*Xj?trm(W8;kvysuWiP5u}(X)lotC`WOh0&*-(Wisaw}a8QlhLo6(XWTmzlYJk zmocD^F|eO8Z~|lCM8?2Lj6o9_gC>F8A3T{ccnV|4RK}2LjG@yRLuW9C&0q|h$rv_^ zF?<$d_-uBwU=HgjPRC?Uw`?x2A}+rQ?!a2^kVfvX7VhwN?#NE=s4niP9-inPl?69d z7T#1{bVqf`J=LWT)RsR~Tk%+JQT9|ZQFKYxDR#LKT37?|`uT^vIyZoR#b9vmDf()v(?iz7_L)oF78 zkJJ<)r8pUt8M$!5-N#*39u$gn zR>-(G$wg0`aolam=a?PG9$oC(GH>-!X0d*^l*gJvuFJp95x7~EvFLL3EEhhxt`pq; z%_lBC*86lUSiR`uj?O)w{s`}^n&Wz}@?RH+ochG+^SU~oY!t72t=ZWjXFc({Ty9i< zoXX3Kohi2Ju9xPeP296QFr{gETuR&Xh?K77YDUL{MQ1+I?2fS6ciP3jaR24wmqlki zKfgU-wdbO}>o1>vX;$n~9ltDoY4ItEi|?MFbkSNqMRUkMU-}~Bs z)+NdNzgg`VE)XKX=tVT|2z5{L4{YJjHWS*rIm-E6Ws`IFvdRnm87H fR&jd1QhDAN-h`R^uRUjAU|{fc^>bP0l+XkKAv&s= literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmeAS@N?(olHy`uVBq!ia0y~yU^oE69Lx+145>_WOc@v$BuiW)N`mv#O3D+9QW+dm z@{>{(JaZG%Q-e|yQz{EjrrIztFy0FA32|j$U7=vLLgJC#>aTJ4T9D`W`gLwjjbtZ#N4x^eAqnb0LmM5cOIHO54qe%>- zNi3sjJfm3xqeU{KMGB*38lzP@qg4i@bta=t7NczrqirsuT`r?t9;018qkTT3eF39G z5u;-~4uK+6lW`UY0|SF-NswPKgMfko7&thbzyBTz-gcI+Wnf^o^K@|x zskrs_3VX0{ph#Pyi^G})N%|2R%{L?-ZV(U%Y+lqcfu(WAio}U)uIrV(+3WUaecD+A zmQ6SJem|Gdo_Kp z-*x@+`J3Y19B#kQTkx(t_`-K<>p6dn#rh_GuokuWC9d+nXX1zFdIcZVw{qEhc&=6O z^|eUFqwV?=KCBk6c(Pr4!l%_@70=G=P581}wBp75D22D-njfy%?{qk*AOB#Z{bq-g z`q2+|)-Q6ns2@Io{eMN{rubzF`u|EA+wSjG@UL6#aQc1N1oQe;4(H#;OmN?SS>~@F ztIeOC+&g|0i+=ccTfE@YZP5>((!~ot`|Cb zb^PFdrt-VXypJEaJ95?T2$v)Ws{O#V4Ednvz`|Pb} zf4VOe!@bWom-}e`tT2YtP3!%mny=z+}-K6mV2ZEt5Icwo(g z%Zr!wX>UAuh9QhqUj7<;)5IBxDQpH>KRz6|eoM6P*rQ_`EdPlXYgN?$U-3W2|NYf9 z3~H?J-^u1!lq%j3KgpL6_~F~TxcJXA&m~w0?N0Rh)m167XXobE9?sqiHmlnth&$|i z@b&H6->;epMCgPeb()B(Tr|RH~O+1PwlVxdT!ROU2OGQ3PvhLeQAwP zO|AMWqpQ}lM;-?Ix_3Z(ZoZK*%HiZ|EUDeVf^rGH<21r@EM zSf|zZ`p5aNP1ut4@|gFs0GYW>Kcl68^QPJ_Y + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..cb1ef880 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 00000000..89176ef4 --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,21 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 00000000..f018a618 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..afa1e8eb --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 00000000..a439442c --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,25 @@ +pluginManagement { + val flutterSdkPath = run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.7.0" apply false + id("org.jetbrains.kotlin.android") version "1.8.22" apply false +} + +include(":app") diff --git a/api/feedServices.ts b/api/feedServices.ts deleted file mode 100644 index f1d3d444..00000000 --- a/api/feedServices.ts +++ /dev/null @@ -1,67 +0,0 @@ -// src/api/videoService.ts -import { PostProps, VideoProps } from '@/types/Interfaces'; - -const extractHashtags = (text: string): string[] => { - if (!text) return []; - const match = text.match(/#[\w]+/g); - return match ? match.map(tag => tag.replace('#', '')) : []; -}; - -export const fetchTrendingPosts = async ( - mediaType: "video" | "image" -): Promise => { - try { - const res = await fetch( - 'https://public.api.bsky.app/xrpc/app.bsky.feed.getFeed?feed=at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot&limit=100' - ); - const data = await res.json(); - if (!data || !data.feed) return []; - - const posts: PostProps[] = data.feed - .map((item: any) => item?.post) - .filter((post: PostProps | undefined): post is PostProps => !!post) - .filter((post: PostProps) => { - const embed = post.embed; - if (!embed) return false; - if (mediaType === "video") { - return ( - embed.$type === "app.bsky.embed.video#view" || - embed.$type === "app.bsky.embed.video" - ); - } else if (mediaType === "image") { - return ( - embed.$type === "app.bsky.embed.images#view" || - embed.$type === "app.bsky.embed.images" - ); - } - return false; - }); - - return posts; - } catch (error) { - console.error("Error fetching trending posts:", error); - return []; - } -}; - -export const fetchPostThread = async ( - author: string, - postId: string -): Promise => { - try { - const res = await fetch( - `https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?uri=at://did:plc:${author}/app.bsky.feed.post/${postId}&depth=10` - ); - const data = await res.json(); - if (!data || !data.thread) return []; - - const posts: PostProps[] = data.thread - .map((item: any) => item?.post) - .filter((post: PostProps | undefined): post is PostProps => !!post); - - return posts; - } catch (error) { - console.error("Error fetching post thread:", error); - return []; - } -}; \ No newline at end of file diff --git a/api/notificationServices.ts b/api/notificationServices.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/api/pdsAuth.ts b/api/pdsAuth.ts deleted file mode 100644 index 1cd9e57a..00000000 --- a/api/pdsAuth.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { PROD_PDS } from "@/constants/atproto"; -import { createSimpleDIDResolver } from "./simpleDIDResolver"; -import sessionManager from "./sessionManager"; -import { Agent, CredentialSession } from "@atproto/api"; - -// Create the resolver once at module level -const resolver = createSimpleDIDResolver(); - -/** - * Login to an AT Protocol PDS (Personal Data Server) using a handle and password - * - * @param handle - The user handle (e.g., username.bsky.social) - * @param password - The user's password - * @returns The session data object or null if login failed - */ -export const pdsLogin = async (handle: string, password: string) => { - try { - const resolverAgent = new Agent(new URL(PROD_PDS)); - const handleRes = await resolverAgent.resolveHandle({ - handle: handle, - }); - - if (!handleRes.success) { - throw new Error("Handle not found"); - } - - // Use the new resolver to get the DID doc - const didDoc = await resolver.resolveHandleToDidDoc(handle); - - // Get the PDS URL from the DID document service endpoints - const pdsUrl = didDoc.pds; - - if (!pdsUrl) { - throw new Error("Could not find PDS URL for this account"); - } - - const pdsCredentialSession = new CredentialSession(new URL(pdsUrl)); - - const agent = new Agent(pdsCredentialSession); - - const { success, data } = await agent.com.atproto.server.createSession({ - identifier: handle, - password, - }); - - if (!success) { - throw new Error("Login was not successful"); - } - - const session = { - ...data, - active: data.active ?? false, - }; - - await sessionManager.setSession(agent, session); - - return data; - } catch (error) { - console.error("Login failed:", error); - throw error; - } -}; - -export const pdsRegister = async ( - email: string, - handle: string, - password: string, - inviteCode?: string -) => { - try { - const cred = new CredentialSession(new URL(PROD_PDS)); - - const agent = new Agent(cred); - - const { success, data } = await agent.com.atproto.server.createAccount({ - email, - handle, - password, - inviteCode, - }); - - if (!success) { - throw new Error("Register was not successful"); - } - - const session = { - ...data, - active: true, - }; - - await sessionManager.setSession(agent, session); - } catch (error) { - console.error("Register failed:", error); - throw error; - } -}; - -/** - * Logout the current user - */ -export const pdsLogout = async () => { - await sessionManager.clearSession(); -}; - -/** - * Get the current session if available - */ -export const getCurrentSession = () => { - return sessionManager.getSession(); -}; - -/** - * Get the current agent if logged in - */ -export const getAgent = () => { - return sessionManager.getAgent(); -}; - -/** - * Check if user is currently logged in - */ -export const isLoggedIn = () => { - return sessionManager.isLoggedIn(); -}; diff --git a/api/profileServices.ts b/api/profileServices.ts deleted file mode 100644 index 8f7bb397..00000000 --- a/api/profileServices.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { PostProps, UserProps } from "@/types/Interfaces"; - -export const getProfile = async (actor: string): Promise => { - try { - const res = await fetch( - `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${actor}` - ); - - if (!res.ok) { - console.error(`Error fetching profile: ${res.status}`); - return null; - } - - const profile: UserProps = await res.json(); - return profile; - } catch (error) { - console.error("Error fetching profile from Bluesky:", error); - return null; - } -}; - -export const getProfileMedia = async ( - actor: string, - mediaType: "video" | "image" -): Promise => { - const res = await fetch( - `https://public.api.bsky.app/xrpc/app.bsky.feed.getAuthorFeed?actor=${actor}&filter=posts_with_media` - ); - - if (!res.ok) { - throw new Error(`Failed to fetch media for actor: ${actor}`); - } - - const data = await res.json(); - - if (!data.feed) { - return []; - } - - const filteredPosts: PostProps[] = data.feed.filter((item: any) => { - const embed = item.post.record.embed; - if (!embed) return false; - if (mediaType === "video") { - return embed.$type === "app.bsky.embed.video"; - } else if (mediaType === "image") { - return embed.$type === "app.bsky.embed.images"; - } - return false; - }); - - return filteredPosts; -}; diff --git a/api/sessionManager.ts b/api/sessionManager.ts deleted file mode 100644 index f480b590..00000000 --- a/api/sessionManager.ts +++ /dev/null @@ -1,218 +0,0 @@ -import EventEmitter from "events"; -import { Agent, AtpSessionData, CredentialSession } from "@atproto/api"; -import * as SecureStore from 'expo-secure-store'; - -export enum SessionEventType { - CREATE = "session:create", - REFRESH = "session:refresh", - EXPIRED = "session:expired", - DELETE = "session:delete", -} - -/** - * SessionManager handles storing and managing AT Protocol sessions - * It maintains the session data globally and emits events when the session changes - */ -class SessionManager extends EventEmitter { - private agent: Agent | null = null; - private sessionData: AtpSessionData | null = null; - private refreshTimeout: NodeJS.Timeout | null = null; - private persistKey: string = "atproto_session"; - private pdsUrl: string = ""; - - constructor() { - super(); - // Immediately invoke an async function to handle async initialization - (async () => { - await this.loadSession(); - })().catch(error => { - console.error('Failed to load session during initialization:', error); - }); - } - - /** - * Get the current Agent instance - */ - getAgent() { - return this.agent; - } - - /** - * Get the current session data - */ - getSession() { - return this.sessionData; - } - - /** - * Store a new session - */ - async setSession(agent: Agent, session: AtpSessionData): Promise { - this.agent = agent; - this.sessionData = session; - this.pdsUrl = session.accessJwt - ? JSON.parse(atob(session.accessJwt.split(".")[1])).aud.replace( - "did:web:", - "" - ) - : ""; - await this.persistSession(); - // this.scheduleRefresh(); - this.emit(SessionEventType.CREATE, session); - } - - // /** - // * Refresh the current session - // */ - // async refreshSession(): Promise { - // if (!this.agent || !this.sessionData) { - // return null; - // } - - // try { - // // For refreshing with the Agent class - // if (this.sessionData.refreshJwt) { - // // Use the refresh method if available - // if (typeof this.agent.refreshSession === "function") { - // const result = await this.agent.refreshSession(); - // if (result && result.data) { - // this.sessionData = result.data; - // this.persistSession(); - // this.scheduleRefresh(); - // this.emit(SessionEventType.REFRESH, result.data); - // return result.data; - // } - // } else { - // const cred = new CredentialSession(new URL(this.sessionData.pds)); - // } - // } - - // // If refresh failed or isn't available - // this.clearSession(); - // this.emit(SessionEventType.EXPIRED); - // return null; - // } catch (error) { - // console.error("Failed to refresh session:", error); - // this.clearSession(); - // this.emit(SessionEventType.EXPIRED); - // return null; - // } - // } - - /** - * Clear the current session (logout) - */ - async clearSession(): Promise { - if (this.refreshTimeout) { - clearTimeout(this.refreshTimeout); - this.refreshTimeout = null; - } - - this.agent = null; - this.sessionData = null; - this.pdsUrl = ""; - await this.removePersistedSession(); - this.emit(SessionEventType.DELETE); - } - - /** - * Check if a user is currently logged in - */ - isLoggedIn(): boolean { - return !!this.sessionData && !!this.agent; - } - - /** - * Schedule a session refresh before it expires - */ - // private scheduleRefresh(): void { - // if (this.refreshTimeout) { - // clearTimeout(this.refreshTimeout); - // } - - // // If no session, don't schedule refresh - // if (!this.sessionData) return; - - // // Default refresh time - 30 minutes before expiry - // // Since we don't have a specific expiry time from the session, - // // we'll use a conservative 23.5 hour refresh interval by default - // const refreshInterval = 23.5 * 60 * 60 * 1000; // 23.5 hours - - // // Schedule refresh - // this.refreshTimeout = setTimeout( - // () => this.refreshSession(), - // refreshInterval - // ); - // } - - /** - * Persist session to storage using SecureStore - */ - private async persistSession(): Promise { - if (this.sessionData) { - try { - await SecureStore.setItemAsync( - this.persistKey, - JSON.stringify(this.sessionData) - ); - } catch (error) { - console.error('Error saving session to SecureStore:', error); - } - } - } - - /** - * Remove persisted session from SecureStore - */ - private async removePersistedSession(): Promise { - try { - await SecureStore.deleteItemAsync(this.persistKey); - } catch (error) { - console.error('Error removing session from SecureStore:', error); - } - } - - /** - * Load session from SecureStore on startup - */ - private async loadSession(): Promise { - try { - const savedSession = await SecureStore.getItemAsync(this.persistKey); - - if (savedSession) { - this.sessionData = JSON.parse(savedSession); - - // Create a new agent with the saved data - if (this.sessionData && this.sessionData.did) { - // Extract the PDS URL from the access token - this.pdsUrl = this.sessionData.accessJwt - ? JSON.parse(atob(this.sessionData.accessJwt.split(".")[1])).aud.replace( - "did:web:", - "" - ) - : ""; - - if (!this.pdsUrl) { - throw new Error("Could not find PDS URL for this account"); - } - const cred = new CredentialSession(new URL(`https://${this.pdsUrl}`)); - cred.resumeSession(this.sessionData); - - const agent = new Agent(cred); - - // Restore the session - this.agent = agent; - // this.scheduleRefresh(); - } - } - } catch (error) { - console.error('Error loading session from SecureStore:', error); - // Clear any invalid or corrupted session data - this.removePersistedSession(); - } - } -} - -// Create a singleton instance -export const sessionManager = new SessionManager(); -export default sessionManager; diff --git a/api/simpleDIDResolver.ts b/api/simpleDIDResolver.ts deleted file mode 100644 index 10b45f7d..00000000 --- a/api/simpleDIDResolver.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { Agent } from "@atproto/api"; -import { AtprotoData } from "@atproto/identity"; -import sessionManager from "./sessionManager"; -import { PROD_PDS } from "@/constants/atproto"; - -// Interface for BidirectionalResolver to avoid circular imports -export interface DIDResolver { - resolveDidToHandle(did: string): Promise; - resolveHandleToDidDoc(handle: string): Promise; - resolveDidsToHandles(dids: string[]): Promise>; -} - -// Cache durations -const ONE_HOUR = 60 * 60 * 1000; -const ONE_DAY = ONE_HOUR * 24; - -// Simple implementation of caches with expiration -class SimpleCache { - private cache = new Map(); - private ttl: number; - - constructor(ttlMs: number) { - this.ttl = ttlMs; - } - - get(key: string): T | undefined { - const item = this.cache.get(key); - if (!item) return undefined; - - if (Date.now() > item.expiry) { - this.cache.delete(key); - return undefined; - } - - return item.value; - } - - set(key: string, value: T): void { - this.cache.set(key, { - value, - expiry: Date.now() + this.ttl, - }); - } - - delete(key: string): void { - this.cache.delete(key); - } -} - -/** - * Create a simple DID resolver with caching that implements DIDResolver - */ -export function createSimpleDIDResolver(): DIDResolver { - // Create caches - const didDocCache = new SimpleCache(ONE_HOUR); - const handleToDIDCache = new SimpleCache(ONE_DAY); - const didToHandleCache = new SimpleCache(ONE_DAY); - - // Helper function to get agent or create a new one - const getAgent = () => { - const agent = sessionManager.getAgent(); - if (agent) return agent; - return new Agent(new URL(PROD_PDS)); - }; - - // Helper function to resolve DID document (not exposed in interface) - const resolveDidDocHelper = async (did: string): Promise => { - // Check cache first - const cachedDoc = didDocCache.get(did); - if (cachedDoc) return cachedDoc; - - try { - // Get agent - const agent = getAgent(); - - // Use plc.directory as fallback - const didResponse = await fetch(`https://plc.directory/${did}`); - - if (!didResponse.ok) { - throw new Error(`Failed to resolve DID document for ${did}`); - } - - const didData = await didResponse.json(); - const atprotoData: AtprotoData = { - did: didData.did, - handle: didData.alsoKnownAs?.[0]?.replace('at://', '') || did, - pds: didData.service?.find((s: any) => s.id === '#atproto_pds')?.serviceEndpoint || '', - signingKey: didData.verificationMethod?.[0]?.publicKeyMultibase || '', - }; - - // Cache the document - didDocCache.set(did, atprotoData); - - return atprotoData; - } catch (error) { - console.error("Error resolving DID document:", error); - throw error; - } - }; - - // The object that implements DIDResolver - return { - /** - * Resolve a DID to a handle with caching - */ - async resolveDidToHandle(did: string): Promise { - // Check cache first - const cachedHandle = didToHandleCache.get(did); - if (cachedHandle) return cachedHandle; - - try { - // Resolve DID document using helper function - const didDoc = await resolveDidDocHelper(did); - - // Cache the handle - if (didDoc.handle) { - didToHandleCache.set(did, didDoc.handle); - return didDoc.handle; - } - - return did; // Fallback to DID if no handle - } catch (error) { - console.error("Error resolving DID to handle:", error); - return did; // Return the DID as fallback - } - }, - - /** - * Resolve a handle to a DID document with caching - */ - async resolveHandleToDidDoc(handle: string): Promise { - try { - // Check if we already have the DID in cache - const cachedDid = handleToDIDCache.get(handle); - if (cachedDid) { - // Check if we have the DID doc cached - const cachedDoc = didDocCache.get(cachedDid); - if (cachedDoc) return cachedDoc; - } - - // Get agent - const agent = getAgent(); - - // Resolve handle to DID - const didResult = await agent.resolveHandle({ handle }); - const did = didResult.data.did; - - // Cache the handle to DID mapping - handleToDIDCache.set(handle, did); - - // Then resolve the DID document - return await resolveDidDocHelper(did); - } catch (error) { - console.error("Error resolving handle to DID doc:", error); - throw new Error(`Failed to resolve handle: ${handle}`); - } - }, - - /** - * Resolve multiple DIDs to handles - */ - async resolveDidsToHandles(dids: string[]): Promise> { - const didHandleMap: Record = {}; - - // Use Promise.all to resolve all DIDs in parallel - const resolves = await Promise.all( - dids.map((did) => this.resolveDidToHandle(did).catch(() => did)) - ); - - // Map results to DIDs - for (let i = 0; i < dids.length; i++) { - didHandleMap[dids[i]] = resolves[i]; - } - - return didHandleMap; - } - }; -} \ No newline at end of file diff --git a/app.json b/app.json deleted file mode 100644 index 1a059422..00000000 --- a/app.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "expo": { - "name": "spark-social-app", - "slug": "spark-social-app", - "version": "1.0.0", - "orientation": "portrait", - "icon": "/assets/images/icon.png", - "scheme": "myapp", - "userInterfaceStyle": "automatic", - "newArchEnabled": true, - "ios": { - "supportsTablet": true - }, - "android": { - "adaptiveIcon": { - "foregroundImage": "/assets/images/adaptive-icon.png", - "backgroundColor": "#ffffff" - } - }, - "web": { - "bundler": "metro", - "output": "static", - "favicon": "/assets/images/favicon.png" - }, - "plugins": [ - "expo-router", - "react-native-imglysdk", - [ - "expo-splash-screen", - { - "image": "/assets/images/splash-icon.png", - "imageWidth": 200, - "resizeMode": "contain", - "backgroundColor": "#ffffff" - } - ], - "expo-video", - "react-native-video", - "expo-secure-store" - ], - "experiments": { - "typedRoutes": true - } - } -} diff --git a/app/(auth)/Login.tsx b/app/(auth)/Login.tsx deleted file mode 100644 index a687a6d6..00000000 --- a/app/(auth)/Login.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import ActionButton from '@/components/global/ActionButton'; -import InputArea from '@/components/global/InputArea'; -import Logo from '@/components/global/Logo'; -import { ThemedText } from '@/components/ThemedText'; -import { Colors } from '@/constants/Colors'; -import { pdsLogin } from '@/api/pdsAuth'; -import { Ionicons } from '@expo/vector-icons'; -import { router } from 'expo-router'; -import React, { useState } from 'react'; -import { View, StyleSheet, useColorScheme, TouchableOpacity, SafeAreaView, Alert, ActivityIndicator } from 'react-native'; - -export default function Login() { - - useColorScheme(); - const colorScheme = useColorScheme(); - - const styles = StyleSheet.create({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: Colors[colorScheme ?? 'light'].background, - }, - text: { - fontSize: 18, - fontWeight: 'bold', - color: '#333', - }, - errorText: { - color: 'red', - marginBottom: 10, - textAlign: 'center', - }, - }); - - const [handle, setHandle] = useState(''); - const [password, setPassword] = useState(''); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); - - const handleLogin = async () => { - if (!handle || !password) { - setError('Please enter both handle and password'); - return; - } - - try { - setLoading(true); - setError(''); - - // Call the pdsLogin function from our API - await pdsLogin(handle, password); - - // If successful, navigate to the app's main screen - router.replace('/(tabs)'); - } catch (err) { - console.error('Login error:', err); - setError(err instanceof Error ? err.message : 'Failed to login. Please try again.'); - } finally { - setLoading(false); - } - }; - - return ( - - router.back()} - style={{ position: 'absolute', top: 80, left: 20, zIndex: 1 }}> - - - - - {error ? {error} : null} - - - - - - - {loading && } - - router.push('/(auth)/Register')} - style={{ marginTop: 20, alignItems: 'center' }} - > - Don't have an account? Register - - - - ); -} \ No newline at end of file diff --git a/app/(auth)/Register.tsx b/app/(auth)/Register.tsx deleted file mode 100644 index c4c012f3..00000000 --- a/app/(auth)/Register.tsx +++ /dev/null @@ -1,404 +0,0 @@ -import ActionButton from '@/components/global/ActionButton'; -import InputArea from '@/components/global/InputArea'; -import Logo from '@/components/global/Logo'; -import { ThemedText } from '@/components/ThemedText'; -import { Colors } from '@/constants/Colors'; -import { pdsRegister } from '@/api/pdsAuth'; -import { Ionicons } from '@expo/vector-icons'; -import { router } from 'expo-router'; -import React, { useCallback, useRef, useState, useMemo, useEffect } from 'react'; -import { View, StyleSheet, useColorScheme, TouchableOpacity, SafeAreaView, Dimensions, ActivityIndicator, Animated } from 'react-native'; -import BottomSheet, { BottomSheetView } from '@gorhom/bottom-sheet'; -import DateTimePicker, { DateTimePickerEvent } from '@react-native-community/datetimepicker'; -import { format } from 'date-fns'; -import Reanimated, { withTiming, useAnimatedStyle, useSharedValue, withSequence, SlideInUp, SlideOutDown } from 'react-native-reanimated'; - -// Define the domain constant -const DOMAIN = '.sprk.so'; - -export default function Register() { - const colorScheme = useColorScheme(); - - const styles = StyleSheet.create({ - container: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: Colors[colorScheme ?? 'light'].background, - width: '100%', - position: 'relative', - }, - formContainer: { - width: '100%', - alignItems: 'center', - flex: 1, - justifyContent: 'center', - }, - backButton: { - position: 'absolute', - top: 80, - left: 20, - zIndex: 1, - }, - logo: { - marginTop: 80, - }, - text: { - fontSize: 18, - fontWeight: 'bold', - }, - errorText: { - color: 'red', - marginBottom: 10, - textAlign: 'center', - }, - datePickerButton: { - flexDirection: 'row', - alignItems: 'center', - borderWidth: 1, - borderColor: Colors[colorScheme ?? 'light'].underlineColor, - borderRadius: 8, - paddingHorizontal: 12, - backgroundColor: Colors[colorScheme ?? 'light'].background, - height: 50, - width: '90%', - marginBottom: 25, - }, - datePickerIcon: { - marginRight: 10, - }, - datePickerText: { - flex: 1, - fontSize: 16, - color: Colors[colorScheme ?? 'light'].text, - }, - datePickerPlaceholder: { - flex: 1, - fontSize: 16, - color: Colors[colorScheme ?? 'light'].textGray, - }, - bottomSheetContent: { - padding: 20, - alignItems: 'center', - backgroundColor: Colors[colorScheme ?? 'light'].background, - paddingBottom: 40, - }, - bottomSheetButton: { - padding: 15, - backgroundColor: Colors[colorScheme ?? 'light'].tint, - borderRadius: 8, - alignItems: 'center', - marginTop: 30, - marginBottom: 30, - width: '80%', - }, - bottomSheetButtonText: { - color: '#fff', - fontWeight: 'bold', - fontSize: 16, - }, - bottomSheetBackground: { - backgroundColor: Colors[colorScheme ?? 'light'].background, - }, - bottomSheetHandle: { - backgroundColor: Colors[colorScheme ?? 'light'].underlineColor, - width: 40, - }, - dateLabelText: { - alignSelf: 'flex-start', - marginLeft: '5%', - marginBottom: 6, - }, - datePickerTitle: { - fontSize: 18, - fontWeight: 'bold', - marginBottom: 20, - }, - chevronIcon: { - marginLeft: 5, - }, - inputStyle: { - width: '90%', - }, - passwordInputStyle: { - width: '90%', - marginBottom: 25, - }, - handleInputContainer: { - flexDirection: 'row', - alignItems: 'center', - width: '90%', - marginBottom: 25, - }, - handleDomain: { - fontSize: 16, - color: Colors[colorScheme ?? 'light'].textGray, - paddingRight: 12, - marginLeft: 5, - }, - toastContainer: { - position: 'absolute', - top: 100, - left: 20, - right: 20, - backgroundColor: 'rgba(255, 0, 0, 0.8)', - padding: 15, - borderRadius: 8, - alignItems: 'center', - zIndex: 1000, - }, - toastText: { - color: 'white', - fontWeight: 'bold', - }, - }); - - const bottomSheetRef = useRef(null); - - // Use fixed height for bottom sheet - const snapPoints = useMemo(() => ['60%'], []); - - const handleSheetChanges = useCallback((index: number) => { - console.log('handleSheetChanges', index); - }, []); - - const openBottomSheet = useCallback(() => { - bottomSheetRef.current?.expand(); - }, []); - - const closeBottomSheet = useCallback(() => { - bottomSheetRef.current?.close(); - }, []); - - const [email, setEmail] = useState(''); - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); - const [inviteCode, setInviteCode] = useState(''); - const [birthDate, setBirthDate] = useState(new Date()); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(''); - const [showToast, setShowToast] = useState(false); - - // Error toast timing effect - useEffect(() => { - if (error) { - setShowToast(true); - const timer = setTimeout(() => { - setShowToast(false); - }, 3000); - return () => clearTimeout(timer); - } - }, [error]); - - const handleDateChange = (event: DateTimePickerEvent, selectedDate: Date | undefined) => { - if (selectedDate) { - setBirthDate(selectedDate); - } - }; - - const confirmDateSelection = () => { - closeBottomSheet(); - }; - - // Function to get the full handle with domain - const getFullHandle = () => { - return `${username}${DOMAIN}`; - }; - - const handleRegister = async () => { - const trimmedEmail = email.trim(); - const trimmedUsername = username.trim(); - - if (!trimmedEmail || !trimmedUsername || !password || !birthDate) { - setError('Please fill in all required fields'); - return; - } - - // Improved email validation with proper trimming - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(trimmedEmail)) { - setError('Please enter a valid email address'); - return; - } - - // Username validation (no dots, no spaces, alphanumeric only) - const usernameRegex = /^[a-zA-Z0-9_-]+$/; - if (!usernameRegex.test(trimmedUsername)) { - setError('Username can only contain letters, numbers, underscores, and hyphens'); - return; - } - - // Password length check - if (password.length < 6) { - setError('Password must be at least 6 characters'); - return; - } - - try { - setLoading(true); - setError(''); - - // Get the full handle with domain and use it for registration - const fullHandle = getFullHandle(); - - // Call the pdsRegister function from our API - await pdsRegister(trimmedEmail, fullHandle, password, inviteCode || undefined); - - // If successful, navigate to the app's main screen - router.replace('/(tabs)'); - } catch (err) { - console.error('Registration error:', err); - setError(err instanceof Error ? err.message : 'Failed to register. Please try again.'); - } finally { - setLoading(false); - } - }; - - return ( - - router.back()} - style={styles.backButton}> - - - - - - {/* Animated Toast for Errors */} - {showToast && ( - - {error} - - )} - - - - - {/* Custom username input with domain appended */} - - - {DOMAIN} - - - - - - - {/* Birth Date Picker Button */} - Birth Date - - - {birthDate ? ( - - {format(birthDate, 'MMMM dd, yyyy')} - - ) : ( - - Select your birth date - - )} - - - - - - {loading && } - - router.push('/(auth)/Login')} - style={{ marginTop: 20, alignItems: 'center' }} - > - Already have an account? Login - - - - {/* Bottom Sheet for Date Picker - outside of the form container */} - - - - Select Your Birth Date - - - - Done - - - - - ); -} \ No newline at end of file diff --git a/app/(screens)/ProfileFeed.tsx b/app/(screens)/ProfileFeed.tsx deleted file mode 100644 index f4df0dc3..00000000 --- a/app/(screens)/ProfileFeed.tsx +++ /dev/null @@ -1,120 +0,0 @@ -// ProfileFeed.tsx -import React, { useEffect, useRef, useState } from 'react'; -import { - StyleSheet, - FlatList, - View, - Dimensions, - TouchableOpacity, - Platform, -} from 'react-native'; -import { ThemedView } from '@/components/ThemedView'; -import VideoScreen from '@/components/Video/VideoScreen'; -import { PostProps } from '@/types/Interfaces'; -import { router, Stack } from 'expo-router'; -import { Ionicons } from '@expo/vector-icons'; -import { useRoute } from '@react-navigation/native'; - -export default function ProfileFeed() { - const flatListRef = useRef(null); - const [postData, setPostData] = useState([]); - const [currentVisibleIndex, setCurrentVisibleIndex] = useState(0); - - const route = useRoute(); - const params = route.params as { videoData?: string; initialIndex?: string }; - - const initialIndex = parseInt(params.initialIndex ?? "0", 10) || 0; - - useEffect(() => { - if (params.videoData) { - try { - const videos = JSON.parse(params.videoData) as PostProps[]; - setPostData(videos); - } catch (error) { - console.error("Error parsing videoData:", error); - } - } - }, [params.videoData]); - - const onViewRef = useRef(({ viewableItems }: any) => { - if (viewableItems.length > 0) { - const index = viewableItems[0].index; - if (index !== null && index !== undefined) { - setCurrentVisibleIndex(index); - } - } - }); - - const viewConfigRef = useRef({ - viewAreaCoveragePercentThreshold: 95, - }); - - const { height: windowHeight } = Dimensions.get('window'); - const TAB_BAR_HEIGHT = 0; - const availableHeight = windowHeight - TAB_BAR_HEIGHT; - - const getItemLayout = (_: any, index: number) => ({ - length: availableHeight, - offset: availableHeight * index, - index, - }); - - return ( - <> - - - router.back()} - style={{ position: 'absolute', top: 50, left: 20, zIndex: 1 }}> - - - `${item.id}-${index}`} - renderItem={({ item, index }) => { - const isActive = index === currentVisibleIndex; - return ( - - - - ); - }} - pagingEnabled - showsVerticalScrollIndicator={false} - onViewableItemsChanged={onViewRef.current} - viewabilityConfig={viewConfigRef.current} - decelerationRate="fast" - initialNumToRender={3} - maxToRenderPerBatch={2} - windowSize={3} - removeClippedSubviews - scrollEventThrottle={16} - style={styles.flatList} - contentContainerStyle={Platform.OS === 'android' ? { paddingBottom: TAB_BAR_HEIGHT } : undefined} - getItemLayout={getItemLayout} - initialScrollIndex={initialIndex} - /> - - - ); -} - -const styles = StyleSheet.create({ - root: { - flex: 1, - backgroundColor: '#000', - }, - flatList: { - flex: 1, - }, - flatListContent: { - paddingBottom: 0, - }, - videoContainer: { - width: '100%', - backgroundColor: '#000', - justifyContent: 'center', - alignItems: 'center', - }, -}); \ No newline at end of file diff --git a/app/(tabs)/CreateScreen.tsx b/app/(tabs)/CreateScreen.tsx deleted file mode 100644 index 68e708a9..00000000 --- a/app/(tabs)/CreateScreen.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Ionicons } from '@expo/vector-icons'; -import { CameraView, CameraType, useCameraPermissions } from 'expo-camera'; -import { useState } from 'react'; -import { Button, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; - -export default function CreateScreen() { - const [facing, setFacing] = useState('back'); - const [permission, requestPermission] = useCameraPermissions(); - - if (!permission) { - return ; - } - - if (!permission.granted) { - return ( - - We need your permission to show the camera -