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
-
-
- );
- }
-
- function toggleCameraFacing() {
- setFacing(current => (current === 'back' ? 'front' : 'back'));
- }
-
- return (
-
-
-
-
-
-
- {}}>
-
-
- {}}>
-
-
-
-
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- message: {
- textAlign: 'center',
- paddingBottom: 10,
- },
- camera: {
- flex: 1,
- alignItems: 'center',
- justifyContent: 'flex-end',
- },
- buttonContainer: {
- flexDirection: 'row',
- backgroundColor: 'transparent',
- alignItems: 'center',
- justifyContent: 'center',
- height: 100,
- width: '100%',
- gap: 30,
- },
- button: {
- alignItems: 'center',
- width: 64,
- height: 64,
- borderRadius: 32,
- justifyContent: 'center',
- },
- text: {
- fontSize: 24,
- fontWeight: 'bold',
- color: 'white',
- },
-});
diff --git a/app/(tabs)/NotificationsScreen.tsx b/app/(tabs)/NotificationsScreen.tsx
deleted file mode 100644
index fa0c1ee1..00000000
--- a/app/(tabs)/NotificationsScreen.tsx
+++ /dev/null
@@ -1,286 +0,0 @@
-import { SafeAreaView, ScrollView, StyleSheet, useColorScheme } from 'react-native';
-import { ThemedText } from '@/components/ThemedText';
-import { ThemedView } from '@/components/ThemedView';
-import ContentWrapper from '@/components/global/ContentWrapper';
-import NotificationItem from '@/components/Notifications/NotificationItem';
-import { NotificationProps } from '@/types/Interfaces';
-import { Colors } from '@/constants/Colors';
-
-const NOTIFICATIONS_DATA: NotificationProps[] = [
- {
- identity: 'like',
- type: { typeName: 'single_user_like' },
- onPress: () => console.log('Alice liked your video'),
- content: {
- title: 'Alice Johnson',
- description: 'liked your video',
- timestamp: new Date(Date.now() - 1 * 3600 * 1000).toISOString(), // 1 hour ago
- users: [
- {
- avatar: 'https://picsum.photos/200/300?random=12',
- displayName: 'Alice Johnson',
- did: 'did:plc:alice001',
- handle: 'alice.j',
- },
- ],
- },
- },
- {
- identity: 'comment',
- type: { typeName: 'single_user_comment' },
- onPress: () => console.log('Bob commented on your video'),
- content: {
- title: 'Bob Smith',
- description: 'commented on your video',
- timestamp: new Date(Date.now() - 2 * 3600 * 1000).toISOString(), // 2 hours ago
- users: [
- {
- avatar: 'https://picsum.photos/200/300?random=13',
- displayName: 'Bob Smith',
- did: 'did:plc:bob002',
- handle: 'bob.s',
- },
- ],
- },
- },
- {
- identity: 'follow',
- type: { typeName: 'single_user_follow' },
- onPress: () => console.log('Charlie followed you'),
- content: {
- title: 'Charlie Davis',
- description: 'followed you',
- timestamp: new Date(Date.now() - 3 * 3600 * 1000).toISOString(), // 3 hours ago
- users: [
- {
- avatar: 'https://picsum.photos/200/300?random=14',
- displayName: 'Charlie Davis',
- did: 'did:plc:charlie003',
- handle: 'charlie.d',
- },
- ],
- },
- },
- {
- identity: 'like',
- type: { typeName: 'multiple_user_like' },
- onPress: () => console.log('Multiple users liked your video'),
- content: {
- title: 'Diana Prince and 3 others',
- description: 'liked your video',
- timestamp: new Date(Date.now() - 4 * 3600 * 1000).toISOString(), // 4 hours ago
- users: [
- {
- avatar: 'https://picsum.photos/200/300?random=15',
- displayName: 'Diana Prince',
- did: 'did:plc:diana004',
- handle: 'diana.p',
- },
- {
- avatar: 'https://picsum.photos/200/300?random=16',
- displayName: 'Edward Norton',
- did: 'did:plc:edward005',
- handle: 'edward.n',
- },
- {
- avatar: 'https://picsum.photos/200/300?random=17',
- displayName: 'Fiona Gallagher',
- did: 'did:plc:fiona006',
- handle: 'fiona.g',
- },
- {
- avatar: 'https://picsum.photos/200/300?random=18',
- displayName: 'George Michael',
- did: 'did:plc:george007',
- handle: 'george.m',
- },
- ],
- },
- },
- {
- identity: 'comment',
- type: { typeName: 'multiple_user_reply' },
- onPress: () => console.log('Multiple users replied to your comment'),
- content: {
- title: 'Hannah, Ian and Jackie',
- description: 'replied to your comment',
- timestamp: new Date(Date.now() - 5 * 3600 * 1000).toISOString(), // 5 hours ago
- users: [
- {
- avatar: 'https://picsum.photos/200/300?random=19',
- displayName: 'Hannah Lee',
- did: 'did:plc:hannah008',
- handle: 'hannah.l',
- },
- {
- avatar: 'https://picsum.photos/200/300?random=20',
- displayName: 'Ian Somerhalder',
- did: 'did:plc:ian009',
- handle: 'ian.s',
- },
- {
- avatar: 'https://picsum.photos/200/300?random=21',
- displayName: 'Jackie Chan',
- did: 'did:plc:jackie010',
- handle: 'jackie.c',
- },
- ],
- },
- },
- {
- identity: 'follow',
- type: { typeName: 'single_user_follow' },
- onPress: () => console.log('Karen followed You'),
- content: {
- title: 'Karen Williams',
- description: 'shared your video',
- timestamp: new Date(Date.now() - 6 * 3600 * 1000).toISOString(), // 6 hours ago
- users: [
- {
- avatar: 'https://picsum.photos/200/300?random=22',
- displayName: 'Karen Williams',
- did: 'did:plc:karen011',
- handle: 'karen.w',
- },
- ],
- },
- },
- {
- identity: 'comment',
- type: { typeName: 'single_user_comment' },
- onPress: () => console.log('Multiple users mentioned you'),
- content: {
- title: 'Leo, Mia and Nina',
- description: 'mentioned you in a comment',
- timestamp: new Date(Date.now() - 7 * 3600 * 1000).toISOString(), // 7 hours ago
- users: [
- {
- avatar: 'https://picsum.photos/200/300?random=23',
- displayName: 'Leo Messi',
- did: 'did:plc:leo012',
- handle: 'leo.m',
- },
- {
- avatar: 'https://picsum.photos/200/300?random=24',
- displayName: 'Mia Khalifa',
- did: 'did:plc:mia013',
- handle: 'mia.k',
- },
- {
- avatar: 'https://picsum.photos/200/300?random=25',
- displayName: 'Nina Dobrev',
- did: 'did:plc:nina014',
- handle: 'nina.d',
- },
- ],
- },
- },
- {
- identity: 'comment',
- type: { typeName: 'single_user_comment' },
- onPress: () => console.log('Olivia commented on your video'),
- content: {
- title: 'Olivia Brown',
- description: 'commented on your video',
- timestamp: new Date(Date.now() - 8 * 3600 * 1000).toISOString(), // 8 hours ago
- users: [
- {
- avatar: 'https://picsum.photos/200/300?random=26',
- displayName: 'Olivia Brown',
- did: 'did:plc:olivia015',
- handle: 'olivia.b',
- },
- ],
- },
- },
- {
- identity: 'follow',
- type: { typeName: 'single_user_follow' },
- onPress: () => console.log('Peter followed you'),
- content: {
- title: 'Peter Parker',
- description: 'followed you',
- timestamp: new Date(Date.now() - 9 * 3600 * 1000).toISOString(), // 9 hours ago
- users: [
- {
- avatar: 'https://picsum.photos/200/300?random=27',
- displayName: 'Peter Parker',
- did: 'did:plc:peter016',
- handle: 'peter.p',
- },
- ],
- },
- },
- {
- identity: 'like',
- type: { typeName: 'multiple_user_like' },
- onPress: () => console.log('Multiple users liked your video'),
- content: {
- title: 'Quincy and 4 others',
- description: 'liked your video',
- timestamp: new Date(Date.now() - 10 * 3600 * 1000).toISOString(), // 10 hours ago
- users: [
- {
- avatar: 'https://picsum.photos/200/300?random=28',
- displayName: 'Quincy Adams',
- did: 'did:plc:quincy017',
- handle: 'quincy.a',
- },
- {
- avatar: 'https://picsum.photos/200/300?random=29',
- displayName: 'Rachel Green',
- did: 'did:plc:rachel018',
- handle: 'rachel.g',
- },
- {
- avatar: 'https://picsum.photos/200/300?random=30',
- displayName: 'Steve Rogers',
- did: 'did:plc:steve019',
- handle: 'steve.r',
- },
- {
- avatar: 'https://picsum.photos/200/300?random=31',
- displayName: 'Tony Stark',
- did: 'did:plc:tony020',
- handle: 'tony.s',
- },
- {
- avatar: 'https://picsum.photos/200/300?random=32',
- displayName: 'Ursula K. Le Guin',
- did: 'did:plc:ursula021',
- handle: 'ursula.k',
- },
- ],
- },
- },
-];
-
-
-export default function NotificationsScreen() {
- const colorScheme = useColorScheme();
-
-const styles = StyleSheet.create({
- container: {
- backgroundColor: Colors[colorScheme ?? 'light'].background,
- height: '100%',
- alignContent: 'center',
- justifyContent: 'center',
- },
- scrollViewContent: {
- flexGrow: 1,
- paddingBottom: 20,
- },
-});
-
- return (
-
-
-
- {NOTIFICATIONS_DATA.map((notification, index) => (
-
- ))}
-
-
-
- );
-}
diff --git a/app/(tabs)/ProfileScreen.tsx b/app/(tabs)/ProfileScreen.tsx
deleted file mode 100644
index 469e443c..00000000
--- a/app/(tabs)/ProfileScreen.tsx
+++ /dev/null
@@ -1,303 +0,0 @@
-import React, { useEffect, useState, useRef, useCallback } from 'react';
-import {
- Platform,
- SafeAreaView,
- ScrollView,
- StyleSheet,
- useColorScheme,
- View,
- Share,
-} from 'react-native';
-import { ThemedText } from '@/components/ThemedText';
-import ContentWrapper from '@/components/global/ContentWrapper';
-import { Colors } from '@/constants/Colors';
-import ProfileInfo from '@/components/Profile/ProfileInfo';
-import { did } from '@/constants/MockData';
-import { UserProps, PostProps } from '@/types/Interfaces';
-import { router, useRouter, useLocalSearchParams } from 'expo-router';
-import { getProfile, getProfileMedia } from '@/api/profileServices';
-import useAtProto from '@/hooks/useAtProto';
-import BottomSheet from '@gorhom/bottom-sheet';
-
-// Import new modular components
-import ProfileHeader from '@/components/Profile/ProfileHeader';
-import ProfileActionButtons from '@/components/Profile/ProfileActionButtons';
-import ProfileTabs from '@/components/Profile/ProfileTabs';
-import ProfileVideoGrid, { padVideosWithPlaceholders } from '@/components/Profile/ProfileVideoGrid';
-import ProfileOptionsBottomSheet from '@/components/Profile/ProfileOptionsBottomSheet';
-
-export default function ProfileScreen() {
- const colorScheme = useColorScheme();
- const route = useRouter();
- const params = useLocalSearchParams();
-
- const { isLoggedIn, session, agent, logout } = useAtProto();
-
- const profileDid = typeof params.did === 'string' && params.did ?
- params.did :
- (isLoggedIn && session ? session.did : did);
-
- const isMine = isLoggedIn && session && profileDid === session.did;
-
- const [userData, setUserData] = useState(null);
- const [videoPosts, setVideoPosts] = useState([]);
- const [activeTab, setActiveTab] = useState<'videos' | 'photos'>('videos');
-
- const bottomSheetRef = useRef(null);
-
- const openBottomSheet = useCallback(() => {
- bottomSheetRef.current?.expand();
- }, []);
-
- const closeBottomSheet = useCallback(() => {
- bottomSheetRef.current?.close();
- }, []);
-
- const loadVideoPosts = async () => {
- try {
- const mediaPosts = await getProfileMedia(profileDid, 'video');
- const posts = mediaPosts.map((item: any) => item.post);
- setVideoPosts(posts);
- } catch (error) {
- console.error('Error loading video posts:', error);
- }
- };
-
- useEffect(() => {
- if (isLoggedIn || (!isLoggedIn && !isMine)) {
- const loadProfileData = async () => {
- try {
- const profileData = await getProfile(profileDid);
- if (profileData) {
- setUserData({
- id: profileData.did,
- did: profileData.did,
- displayName: profileData.displayName || (isMine && session?.handle ? session.handle : ''),
- handle: profileData.handle || (isMine && session?.handle ? session.handle : ''),
- description: profileData.description || '',
- avatar: profileData.avatar || '',
- banner: profileData.banner || '',
- followersCount: profileData.followersCount || 0,
- followsCount: profileData.followsCount || 0,
- postsCount: profileData.postsCount || 0,
- associated: profileData.associated,
- joinedViaStarterPack: profileData.joinedViaStarterPack,
- indexedAt: profileData.indexedAt || '',
- createdAt: profileData.createdAt || '',
- viewer: profileData.viewer,
- labels: profileData.labels || [],
- pinnedPost: profileData.pinnedPost,
- });
- }
- } catch (error) {
- console.error('Error loading profile:', error);
-
- if (isLoggedIn && session && isMine) {
- setUserData({
- id: session.did,
- did: session.did,
- displayName: session.handle || 'My Profile',
- handle: session.handle || '',
- description: '',
- avatar: '',
- banner: '',
- followersCount: 0,
- followsCount: 0,
- postsCount: 0,
- indexedAt: '',
- createdAt: '',
- labels: [],
- });
- }
- }
- };
-
- loadProfileData();
- loadVideoPosts();
- } else if (!isLoggedIn && isMine) {
- setUserData({
- id: '',
- did: '',
- displayName: 'Login ou Registrar',
- handle: 'null',
- description: '',
- avatar: 'https://static.sprk.so/branding/default-profile.png?d',
- banner: '',
- followersCount: 0,
- followsCount: 0,
- postsCount: 0,
- indexedAt: '',
- createdAt: '',
- labels: [],
- });
- }
- }, [isLoggedIn, isMine, session, profileDid]);
-
- const paddedVideoData = padVideosWithPlaceholders(videoPosts);
-
- function handleOpenProfileFeed(post: PostProps) {
- const index = videoPosts.findIndex((video) => video.uri === post.uri);
- route.push({
- pathname: '../ProfileFeed',
- params: {
- videoData: JSON.stringify(videoPosts),
- initialIndex: index.toString(),
- animation: Platform.OS === 'android' ? 'fade' : 'none'
- },
- });
- }
-
- function goTo(route: string) {
- route = route.toLowerCase();
- if (route === 'register') {
- router.push('/(auth)/Register', { relativeToDirectory: true });
- } else {
- router.push('/(auth)/Login', { relativeToDirectory: true });
- }
- }
-
- const handleProfileSettings = () => {
- closeBottomSheet();
- console.log('Navigate to profile settings');
- };
-
- const handleLogout = async () => {
- try {
- closeBottomSheet();
- await logout();
- console.log('User logged out');
- } catch (error) {
- console.error('Error logging out:', error);
- }
- };
-
- const handleTabChange = (tab: 'videos' | 'photos') => {
- setActiveTab(tab);
- };
-
- const handleFollow = () => {
- console.log("followed " + userData?.did);
- };
-
- // New handlers for profile actions
- const handleEdit = () => {
- console.log("Edit profile functionality would be implemented here");
- // TODO: Implement profile editing screen
- };
-
- const handleShareProfile = async () => {
- try {
- if (userData) {
- const profileUrl = `https://sprk.so/${userData.handle}`;
- await Share.share({
- message: `Check out ${userData.displayName}'s profile on Spark: ${profileUrl}`,
- url: profileUrl,
- });
- console.log("Shared profile");
- }
- } catch (error) {
- console.error('Error sharing profile:', error);
- }
- };
-
- const handleFriends = () => {
- console.log("Friends functionality would be implemented here");
- // TODO: Implement friends screen
- };
-
- const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: Colors[colorScheme ?? 'light'].background,
- },
- scrollViewContent: {
- flexGrow: 1,
- paddingBottom: 20,
- },
- profileHeader: {
- alignItems: 'center',
- width: '100%',
- },
- profileHeaderNull: {
- alignItems: 'center',
- width: '100%',
- justifyContent: 'center',
- height: '70%',
- },
- profileContent: {
- marginTop: 20,
- height: '100%',
- },
- });
-
- return (
-
-
-
- {
- if (params.did) {
- router.back();
- }
- }}
- onSettingsPress={openBottomSheet}
- isSettingsVisible={isLoggedIn && isMine}
- />
-
-
- {userData && }
-
- goTo('register')}
- onLogin={() => goTo('login')}
- onFollow={handleFollow}
- onLogout={handleLogout}
- onEdit={handleEdit}
- onShareProfile={handleShareProfile}
- onFriends={handleFriends}
- />
-
-
-
- {(isLoggedIn || (!isLoggedIn && !isMine)) && (
- <>
-
-
-
- >
- )}
-
-
-
-
-
-
- );
-}
-
-// Utility function to navigate to a profile
-export function navigateToProfile(did: string) {
- // If navigating to a profile, always include the DID parameter
- // This allows the correct handling when coming from different contexts
- router.push({
- pathname: "/(tabs)/ProfileScreen",
- params: { did }
- });
-}
\ No newline at end of file
diff --git a/app/(tabs)/SearchScreen.tsx b/app/(tabs)/SearchScreen.tsx
deleted file mode 100644
index 8380f959..00000000
--- a/app/(tabs)/SearchScreen.tsx
+++ /dev/null
@@ -1,178 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { SafeAreaView, ScrollView, StyleSheet, useColorScheme, View } from 'react-native';
-import ContentWrapper from '@/components/global/ContentWrapper';
-import SearchBar from '@/components/Search/SearchBar';
-import FeaturedProfile from '@/components/Search/FeaturedProfile';
-import VideoDisplay from '@/components/global/VideoDisplay';
-import PlaceholderVideoDisplay from '@/components/Profile/PlaceholderVideoDisplay';
-import { Colors } from '@/constants/Colors';
-import { UserProps, PostProps } from '@/types/Interfaces';
-import { fetchTrendingPosts } from '@/api/feedServices';
-import { getProfile } from '@/api/profileServices';
-import { useRouter } from 'expo-router';
-import VideoRowCategory from '@/components/Search/VideoRowCategory';
-import SoundRowsCategory, { SoundProps } from '@/components/Search/SoundRowsCategory';
-import { trendingSounds } from '@/components/Search/mockSoundsData';
-import { Ionicons, FontAwesome5, MaterialCommunityIcons } from '@expo/vector-icons';
-
-// Pad posts so that the grid always has complete rows
-function padPostsWithPlaceholders(
- posts: PostProps[]
-): (PostProps & { isPlaceholder?: boolean })[] {
- const remainder = posts.length % 3;
- const placeholdersNeeded = remainder === 0 ? 0 : 3 - remainder;
-
- const placeholders: (PostProps & { isPlaceholder?: boolean })[] = Array(placeholdersNeeded)
- .fill(null)
- .map((_, i) => ({
- uri: `placeholder-${i}`,
- cid: '',
- author: {
- did: '',
- handle: '',
- displayName: '',
- avatar: '',
- banner: '',
- },
- record: {
- $type: 'app.bsky.feed.post',
- createdAt: '',
- text: '',
- langs: [],
- },
- replyCount: 0,
- repostCount: 0,
- likeCount: 0,
- quoteCount: 0,
- indexedAt: '',
- labels: [],
- isPlaceholder: true,
- }));
- return [...posts, ...placeholders];
-}
-
-export default function SearchScreen() {
- const colorScheme = useColorScheme();
- const router = useRouter();
- const [videoData, setVideoData] = useState([]);
- const [featuredUser, setFeaturedUser] = useState(null);
-
- function handleOpenProfileFeed(postClicked: PostProps) {
- const index = videoData.findIndex((post) => post.uri === postClicked.uri);
- router.push({
- pathname: '../ProfileFeed',
- params: {
- videoData: JSON.stringify(videoData),
- initialIndex: index.toString(),
- },
- });
- }
-
- function handleSoundPress(sound: SoundProps) {
- console.log('Sound pressed:', sound.title);
- // Future implementation: play sound or navigate to sound details
- }
-
- useEffect(() => {
- const loadTrendingData = async () => {
- try {
- const posts = await fetchTrendingPosts('video');
- setVideoData(posts);
- if (posts.length > 0) {
- const did = posts[2]?.author?.did;
- if (did) {
- const profileData = await getProfile(did);
- if (profileData) {
- setFeaturedUser({
- id: profileData.did,
- did: profileData.did,
- displayName: profileData.displayName,
- handle: profileData.handle,
- description: profileData.description,
- avatar: profileData.avatar || '',
- banner: profileData.banner || '',
- followersCount: profileData.followersCount,
- followsCount: profileData.followsCount,
- likes: 0,
- views: 0,
- postsCount: profileData.postsCount,
- associated: profileData.associated,
- joinedViaStarterPack: profileData.joinedViaStarterPack,
- indexedAt: profileData.indexedAt,
- createdAt: profileData.createdAt,
- viewer: profileData.viewer,
- labels: profileData.labels,
- pinnedPost: profileData.pinnedPost,
- });
- }
- }
- }
- } catch (err) {
- console.error(err);
- }
- };
- loadTrendingData();
- }, []);
-
- const paddedVideoData = padPostsWithPlaceholders(videoData);
-
- const styles = StyleSheet.create({
- container: {
- backgroundColor: Colors[colorScheme ?? 'light'].background,
- height: '100%',
- alignContent: 'center',
- justifyContent: 'center',
- },
- scrollViewContent: {
- flexGrow: 1,
- paddingBottom: 20,
- },
- videoGrid: {
- flexDirection: 'row',
- flexWrap: 'wrap',
- justifyContent: 'center',
- },
- });
-
- return (
-
-
-
- {}} />
- {featuredUser && (
- {}} />
- )}
- }
- title="Trending"
- videos={videoData}
- onVideoPress={handleOpenProfileFeed}
- />
- }
- title="Sounds"
- sounds={trendingSounds}
- onSoundPress={handleSoundPress}
- />
-
- {paddedVideoData.map((post, index) => {
- const key = post.uri || `fallback-${index}`;
- if (post.isPlaceholder) {
- return ;
- }
- return (
-
- );
- })}
-
-
-
-
- );
-}
diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx
deleted file mode 100644
index ac7fa1df..00000000
--- a/app/(tabs)/_layout.tsx
+++ /dev/null
@@ -1,192 +0,0 @@
-import { Tabs } from 'expo-router';
-import React, { useEffect, useState } from 'react';
-import { Platform } from 'react-native';
-import { HapticTab } from '@/components/HapticTab';
-import { Ionicons } from '@expo/vector-icons';
-import TabBarBackground from '@/components/ui/TabBarBackground';
-import { Colors } from '@/constants/Colors';
-import { useColorScheme } from '@/hooks/useColorScheme';
-import ProfileIcon from '@/components/global/ProfileIcon';
-import { UserProps } from '@/types/Interfaces';
-import { getProfile } from '@/api/profileServices';
-import { did } from '@/constants/MockData';
-import useAtProto from '@/hooks/useAtProto';
-import { router } from 'expo-router';
-
-export default function TabLayout() {
- const colorScheme = useColorScheme();
-
- const [userData, setUserData] = useState(null);
- const { isLoggedIn, session } = useAtProto();
-
-
-useEffect(() => {
- if (isLoggedIn) {
- const loadProfileData = async () => {
- try {
- const profileData = await getProfile(session.did);
- if (profileData) {
- setUserData({
- id: profileData.did,
- did: profileData.did,
- displayName: profileData.displayName,
- handle: profileData.handle,
- description: profileData.description,
- avatar: profileData.avatar || '',
- banner: profileData.banner || '',
- followersCount: profileData.followersCount,
- followsCount: profileData.followsCount,
- likes: 0,
- views: 0,
- videos: profileData.videos || [],
- postsCount: profileData.postsCount,
- associated: profileData.associated,
- joinedViaStarterPack: profileData.joinedViaStarterPack,
- indexedAt: profileData.indexedAt,
- createdAt: profileData.createdAt,
- viewer: profileData.viewer,
- labels: profileData.labels,
- pinnedPost: profileData.pinnedPost,
- });
- }
- } catch (error) {
- console.error("Error loading profile:", error);
- }
- };
-
- loadProfileData();
- } else {
- setUserData({
- id: '',
- did: '',
- displayName: 'Login or Register',
- handle: 'null',
- description: '',
- avatar: 'https://static.sprk.so/branding/default-profile.png?d',
- banner: '',
- followersCount: 0,
- followsCount: 0,
- postsCount: 0,
- indexedAt: '',
- createdAt: '',
- labels: [],
- });
- }
- }, []);
-
- return (
-
- ,
- }}
- />
- ,
- }}
- />
- ,
- }}
- />
- ,
- }}
- />
- userData ? : null,
- }}
- listeners={({ navigation }) => ({
- tabPress: () => {
- router.replace('/(tabs)/ProfileScreen');
- },
- })}
- />
-
- );
-}
diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx
deleted file mode 100644
index 25e8e4ae..00000000
--- a/app/(tabs)/index.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-import React, { useEffect, useRef, useState } from 'react';
-import {
- StyleSheet,
- FlatList,
- View,
- Dimensions,
- Platform,
-} from 'react-native';
-import { ThemedView } from '@/components/ThemedView';
-import VideoScreen from '@/components/Video/VideoScreen';
-import VideoTop from '@/components/Video/VideoTop';
-import ImageScreen from '@/components/Image/ImageScreen';
-import { PostProps } from '@/types/Interfaces';
-import { fetchTrendingPosts } from '@/api/feedServices';
-import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
-
-export default function HomeScreen() {
- const flatListRef = useRef>(null);
- const [postData, setPostData] = useState([]);
- const [currentVisibleIndex, setCurrentVisibleIndex] = useState(0);
-
- 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 = useBottomTabBarHeight();
- // Calculate available height properly for both platforms
- const availableHeight = windowHeight - TAB_BAR_HEIGHT;
-
- const shuffleArray = (array: any[]) => {
- return array.sort(() => Math.random() - 0.5);
- };
-
- useEffect(() => {
- const loadContent = async () => {
- try {
- const videoPosts = await fetchTrendingPosts('video');
- const imagePosts = await fetchTrendingPosts('image');
- const mergedData = shuffleArray([...videoPosts, ...imagePosts]);
- setPostData(mergedData);
- } catch (error) {
- console.error('Error fetching posts:', error);
- }
- };
-
- loadContent();
- }, []);
-
- return (
-
-
- `${item.cid}-${index}`}
- renderItem={({ item, index }) => {
- const isActive = index === currentVisibleIndex;
- const embedType = item.embed?.$type || '';
-
- return (
-
- {embedType === 'app.bsky.embed.video' ||
- embedType === 'app.bsky.embed.video#view' ? (
-
- ) : embedType === 'app.bsky.embed.images' ||
- embedType === 'app.bsky.embed.images#view' ? (
-
- ) : null}
-
- );
- }}
- pagingEnabled
- showsVerticalScrollIndicator={false}
- onViewableItemsChanged={onViewRef.current}
- viewabilityConfig={viewConfigRef.current}
- decelerationRate="fast"
- initialNumToRender={3}
- maxToRenderPerBatch={2}
- removeClippedSubviews
- scrollEventThrottle={16}
- contentContainerStyle={Platform.OS === 'android' ? { paddingBottom: TAB_BAR_HEIGHT } : undefined}
- style={styles.flatList}
- />
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- flatList: {
- backgroundColor: 'black',
- },
- itemContainer: {
- width: '100%',
- }
-});
diff --git a/app/+not-found.tsx b/app/+not-found.tsx
deleted file mode 100644
index 963b04fb..00000000
--- a/app/+not-found.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { Link, Stack } from 'expo-router';
-import { StyleSheet } from 'react-native';
-
-import { ThemedText } from '@/components/ThemedText';
-import { ThemedView } from '@/components/ThemedView';
-
-export default function NotFoundScreen() {
- return (
- <>
-
-
- This screen doesn't exist.
-
- Go to home screen!
-
-
- >
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- alignItems: 'center',
- justifyContent: 'center',
- padding: 20,
- },
- link: {
- marginTop: 15,
- paddingVertical: 15,
- },
-});
diff --git a/app/_layout.tsx b/app/_layout.tsx
deleted file mode 100644
index 40aa51ca..00000000
--- a/app/_layout.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
-import { useFonts } from 'expo-font';
-import { Stack } from 'expo-router';
-import * as SplashScreen from 'expo-splash-screen';
-import { StatusBar } from 'expo-status-bar';
-import { useEffect, useState } from 'react';
-import { GestureHandlerRootView } from 'react-native-gesture-handler';
-
-import { useColorScheme } from '@/hooks/useColorScheme';
-
-SplashScreen.preventAutoHideAsync();
-
-export default function RootLayout() {
- const colorScheme = useColorScheme();
- const [loaded] = useFonts({
- Nunito: require('../assets/fonts/Nunito-VariableFont_wght.ttf'),
- });
-
- useEffect(() => {
- if (loaded) {
- SplashScreen.hideAsync();
- }
- }, [loaded]);
-
- if (!loaded) {
- return null;
- }
-
- return (
-
-
-
- {/* Auth Screens */}
-
-
- {/* Main App Screens */}
-
-
-
-
-
- );
-}
\ No newline at end of file
diff --git a/assets/fonts/Nunito-VariableFont_wght.ttf b/assets/fonts/Nunito-VariableFont_wght.ttf
deleted file mode 100644
index 0a00f63fe98df5edcbdf872cf7e686e082a0cc6c..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 277844
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