diff --git a/app.json b/app.json index 6a1d946..4ae4b65 100644 --- a/app.json +++ b/app.json @@ -1,10 +1,10 @@ { "expo": { - "name": "Fact checker", - "slug": "fake-check-bd", + "name": "Fraud Guard", + "slug": "fraud-guard", "version": "1.0.0", "orientation": "portrait", - "icon": "./assets/images/fraud_logo.jpeg", + "icon": "./assets/images/guard.png", "scheme": "myapp", "userInterfaceStyle": "automatic", "newArchEnabled": true, @@ -13,7 +13,7 @@ }, "android": { "adaptiveIcon": { - "foregroundImage": "./assets/images/fraud_logo.jpeg", + "foregroundImage": "./assets/images/guard.png", "backgroundColor": "#ffffff" }, "package": "com.smfahimhossen.android" @@ -28,7 +28,7 @@ [ "expo-splash-screen", { - "image": "./assets/images/fraud_logo.jpeg", + "image": "./assets/images/guard.png", "imageWidth": 200, "resizeMode": "contain", "backgroundColor": "#ffffff" diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 093a4a0..fd775e0 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -29,31 +29,14 @@ export default function TabLayout() { }}> ( - - ), - }} - /> - ( + ( + + ), + }} + /> ); } diff --git a/app/(tabs)/explore.tsx b/app/(tabs)/explore.tsx index eda7cc8..108a8ab 100644 --- a/app/(tabs)/explore.tsx +++ b/app/(tabs)/explore.tsx @@ -1,532 +1,398 @@ +import { WebView } from "react-native-webview"; +import { useRef, useState } from "react"; +import { Alert, StyleSheet, View } from "react-native"; -import { - View, - Text, - Image, - StyleSheet, - Dimensions, - Platform, - StatusBar, - FlatList, - TouchableOpacity, - SafeAreaView, - Modal, - ScrollView, - ActivityIndicator, - Linking, -} from "react-native"; -import { useState, useEffect } from "react"; -import { BlurView } from "expo-blur"; -import { useRouter } from "expo-router"; -import { IconSymbol } from "@/components/ui/IconSymbol"; -import { useColorScheme } from "@/hooks/useColorScheme"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; - -const { width, height } = Dimensions.get("window"); - -// API endpoint - use your own IP address here -const API_URL = "https://dev.dashboard.planpostai.com/news/api/news"; - -// TypeScript interfaces -interface Paragraph { - content: string; -} - -interface NewsItem { - id: number; - created_at: string; - title: string; - description: string; - image_url: string; - image_path?: string; - paragraphs: string[]; -} - -interface AdItem { - id: string; - isAd: boolean; - adIndex: number; -} - -type FeedItem = NewsItem | AdItem; - -// Type guard to check if an item is an ad -const isAd = (item: FeedItem): item is AdItem => { - return (item as AdItem).isAd === true; -}; - -// Single ad data -const singleAd = { - id: 1, - title: "Internet Packages", - description: - "Get the latest data offers...", - image: - "https://cdn01da.grameenphone.com/sites/default/files/2023-09/GP_013_New_Number_Series-1060-x-764.jpg", - url: "https://www.premium-headphones.com", -}; - -export default function NewsScreen(): JSX.Element { - const colorScheme = useColorScheme(); - const router = useRouter(); - const insets = useSafeAreaInsets(); - const [selectedNews, setSelectedNews] = useState(null); - const [modalVisible, setModalVisible] = useState(false); - const [newsData, setNewsData] = useState([]); - const [processedData, setProcessedData] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - // Fetch news data from API - useEffect(() => { - fetchNews(); - }, []); - - // Process news data to add ads after every 4 news items - useEffect(() => { - const processed: FeedItem[] = []; - - newsData.forEach((item, index) => { - processed.push(item); - if ((index + 1) % 4 === 0 && index !== newsData.length - 1) { - processed.push({ - id: `ad-${Math.floor(index / 4)}`, - isAd: true, - adIndex: Math.floor(index / 4), - }); - } - }); - - setProcessedData(processed); - }, [newsData]); - - const fetchNews = async (): Promise => { - setLoading(true); - setError(null); +export default function HomeScreen() { + const webviewRef = useRef(null); + const [postData, setPostData] = useState([]); + const [isApiCallInProgress, setIsApiCallInProgress] = useState(false); + // Function to call the fact-check API + const checkFacts = async (caption, imageUrl) => { try { - // Set a timeout to prevent hanging requests - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 10000); - const response = await fetch(API_URL, { - signal: controller.signal, + const response = await fetch('https://factcheck.planpostai.com/check-facts', { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ query: caption, url:imageUrl }), }); - clearTimeout(timeoutId); + const responseText = await response.text(); + console.log('Raw API Response:', responseText); if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); + throw new Error(`HTTP error! status: ${response.status} - ${responseText}`); } - const responseJson = await response.json(); + const result = JSON.parse(responseText); + let color; + if (result?.verdict === 'True') { + color = "#2E7D32"; + } else if (result?.verdict === 'False') { + color = "#D32F2F"; + } else { + color = "#F9A825"; + } - // Extract data from the nested structure - const data = responseJson.data || responseJson; + // Send the result back to WebView to show toast + webviewRef.current?.injectJavaScript(` + (function() { + showToast('${result.evidence.replace(/'/g, "\\'")}', 10000, '${color}'); + return true; + })(); + `); - if (!data || !Array.isArray(data)) { - throw new Error("Invalid data format received from API"); - } + return result; + } catch (error) { + console.error('API Error Details:', { + message: error.message, + stack: error.stack, + name: error.name + }); - // Process the data to ensure it has the required format - const processedData: NewsItem[] = data.map((item: any) => ({ - ...item, - // If paragraphs don't exist, create them from description - paragraphs: item.paragraphs || [ - item.description.substring(0, item.description.length / 3), - item.description.substring( - item.description.length / 3, - (2 * item.description.length) / 3 - ), - item.description.substring((2 * item.description.length) / 3), - ], - })); + // Show error in WebView + webviewRef.current?.injectJavaScript(` + (function() { + showToast('Error: ${error.message.replace(/'/g, "\\'")}', 5000, true); + return true; + })(); + `); - setNewsData(processedData); - } catch (err) { - console.error("Failed to fetch news:", err); - setError( - err instanceof Error ? err.message : "An unknown error occurred" - ); - setNewsData([]); - } finally { - setLoading(false); + throw error; } }; - const formatDate = (dateString: string): string => { - const date = new Date(dateString); - return date.toLocaleDateString("en-US", { - year: "numeric", - month: "long", - day: "numeric", - }); - }; + const handleMessage = async (event) => { + try { + const data = JSON.parse(event.nativeEvent.data); + switch (data.type) { + case 'postData': + setPostData(prevData => [...prevData, { + caption: data.caption, + imageUrls: data.imageUrls, + postId: data.postId + }]); - // Function to truncate text to 99% and add ellipsis - const truncateDescription = (text: string): string => { - const truncateLength = Math.floor(text.length * 0.99); - return text.substring(0, truncateLength) + "..."; - }; + if (isApiCallInProgress) { + return; + } - const openNewsModal = (news: NewsItem): void => { - setSelectedNews(news); - setModalVisible(true); - }; + setIsApiCallInProgress(true); - const closeNewsModal = (): void => { - setModalVisible(false); - }; + try { + const result = await checkFacts(data.caption, data.imageUrls[0]); - // Navigate to ad website - const navigateToAdPage = (url: string): void => { - // Open the URL in the device's browser - Linking.openURL(url).catch((err) => - console.error("Couldn't open URL: ", err) - ); - }; + setIsApiCallInProgress(false); + webviewRef.current?.injectJavaScript(` + (function() { + enableAllButtons(); + const button = document.querySelector('button[data-post-id="${data.postId}"]'); + if (button) { + button.textContent = 'Processed'; + button.style.background="green"; + button.style.color="white"; + button.style.padding="5px 5px"; + button.style.opacity = '0.7'; + button.disabled = true; + } + return true; + })(); + `); + } catch (error) { + setIsApiCallInProgress(false); + webviewRef.current?.injectJavaScript(` + (function() { + const svgCode=\` + + + + + \`; + enableAllButtons(); + const button = document.querySelector('button[data-post-id="${data.postId}"]'); + if (button) { + button.disabled = false; + button.style.opacity = '1'; + button.innerHTML = svgCode; + } + return true; + })(); + `); + } finally { + setIsApiCallInProgress(false); + } + break; - // Advertisement component with full screen layout - const AdvertisementCard = ({ index }: { index: number }): JSX.Element => { - return ( - - - SPONSORED - - - {/* Full screen ad with vertical layout */} - navigateToAdPage(singleAd.url)} - activeOpacity={0.9} - > - {/* Image first */} - - - - - {/* Content below image */} - - {singleAd.title} - - {singleAd.description} - - - - - ); - }; - - const renderNewsItem = ({ item }: { item: FeedItem }): JSX.Element => { - if (isAd(item)) { - return ; + case 'error': + console.error(`WebView Error: ${data.message}`); + break; + } + } catch (error) { + console.error('Error handling message:', error); } - - // Truncate description to 99% and add ellipsis - const truncatedDescription = truncateDescription(item.paragraphs[0]); - - return ( - - {/* News image - 30% of screen height */} - - - - - {/* Content area - 70% of screen height */} - - {Platform.OS === "ios" ? ( - - - - {formatDate(item.created_at)} - - {/* Title first as requested */} - {item.title} - - {/* Show 99% of description with ellipsis */} - {truncatedDescription} - - {/* Read more button positioned on the right */} - - openNewsModal(item)} - > - Read more - - - - - - - ) : ( - - - - {formatDate(item.created_at)} - - {/* Title first as requested */} - {item.title} - - {/* Show 99% of description with ellipsis */} - {truncatedDescription} - - {/* Read more button positioned on the right */} - - openNewsModal(item)} - > - Read more - - - - - - - )} - - - ); }; - const renderErrorView = (): JSX.Element => ( - - - - Connection Error - - - {error} - - - Retry - - - ); + const injectedJavaScript = ` + (function() { + // Add toast CSS + const style = document.createElement('style'); + style.innerHTML = \` + #toast-container { + position: fixed; + top: 50px; + right: 20px; + left: 20px; + z-index: 999999; + display: flex; + flex-direction: column; + gap: 10px; + pointer-events: auto; + } + .toast { + background-color: #1db146; + color: #fff; + font-weight: 800; + padding: 12px 16px; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); + font-size: 16px; + opacity: 1; + position: relative; + width: 90%; + z-index: 99999999999; + transition: opacity 0.5s ease; + } + .toast.fade-out { + opacity: 0; + } + .error-toast { + background-color: #ff4444; + } + \`; + document.head.appendChild(style); + + // Function to disable all buttons + window.disableAllButtons = function() { + document.querySelectorAll("button").forEach((btn) => { + btn.disabled = true; + btn.style.opacity = "0.5"; + btn.style.cursor = "not-allowed"; + }); + }; + + // Function to enable all buttons + window.enableAllButtons = function() { + document.querySelectorAll("button").forEach((btn) => { + if (btn.textContent !== "Processed") { + btn.disabled = false; + btn.style.opacity = "1"; + btn.style.cursor = "pointer"; + } + }); + }; + + // Toast notification function + window.showToast = function showToast(message, duration, color = '#333') { + // Create the toast element + const toast = document.createElement('div'); + toast.textContent = message; + + // Apply basic styles + toast.style.position = 'fixed'; + toast.style.top = '5px'; + toast.style.left = '50%'; + toast.style.fontSize="16px"; + toast.style.fontWeight="800"; + toast.style.transform = 'translateX(-50%)'; + toast.style.padding = '10px 20px'; + toast.style.color = '#fff'; + toast.style.backgroundColor = color; // Dynamic background color + toast.style.borderRadius = '8px'; + toast.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.3)'; + toast.style.zIndex = '9999'; + toast.style.opacity = '1'; + toast.style.transition = 'opacity 0.5s ease-in-out'; + toast.style.width = '85%'; + + // Add toast to the DOM + document.body.appendChild(toast); + + // Auto-remove the toast after the specified duration + setTimeout(() => { + toast.style.opacity = '0'; // Fade out + setTimeout(() => { + document.body.removeChild(toast); // Remove from DOM + }, 500); // Match fade-out duration + }, duration); + }; + + // Function to handle "See More" click + async function handleSeeMoreClick(seeMoreElement, post, button, initialCaption, initialImageURLs, postId) { + return new Promise((resolve) => { + setTimeout(() => { + seeMoreElement.click(); + + setTimeout(() => { + const captionElement = post.querySelector( + "div.m div.m div[data-type='text'] div.native-text" + ); + fullCaption = captionElement ? captionElement.textContent.trim() : initialCaption; + + // Send data to React Native + window.ReactNativeWebView.postMessage(JSON.stringify({ + type: 'postData', + caption: fullCaption, + imageUrls: initialImageURLs, + postId + })); + + resolve(); + }, 2000); + }, 100); + }); + } + + // Function to collect post data immediately + function collectDataImmediately(post, button, initialCaption, initialImageURLs, postId) { + const captionElement = post.querySelector( + "div.m div.m div[data-type='text'] div.native-text" + ); + const caption = captionElement ? captionElement.textContent.trim() : initialCaption; + + // Send data to React Native + window.ReactNativeWebView.postMessage(JSON.stringify({ + type: 'postData', + caption, + imageUrls: initialImageURLs, + postId + })); + } + + const svgCode=\` + + + + + \` + + + // Function to collect post data and send to React Native + function collectPostData(post) { + if (post.dataset.processed === "true") return; + + // Get the caption + const captionElement = post.querySelector( + "div.m div.m div[data-type='text'] div.native-text" + ); + const caption = captionElement ? captionElement.textContent.trim() : null; + + // Get the first image (if exists) + const singleImageElement = post.querySelector("div.m.bg-s13 img"); + const singleImageURL = singleImageElement && singleImageElement.src ? singleImageElement.src : null; + + // Collect all image elements + const imageElements = post.querySelectorAll("div.m.bg-s13 img"); + const imageURLs = Array.from(imageElements) + .filter(img => img.src) + .map(img => img.src); + + post.dataset.processed = "true"; + + // Prioritize using a single image if only one exists, otherwise use all images + let imagesToUse = []; + if (singleImageURL) { + imagesToUse = [singleImageURL]; + } else if (imageURLs.length > 0) { + imagesToUse = imageURLs; + } + + // Create button if there's a caption or at least one image + if ((caption && caption.length > 50) || imagesToUse.length > 0) { + const postId = 'post_' + Math.random().toString(36).substr(2, 9); + createButton(post, caption, imagesToUse, postId); + } + } + + + // Create check button + function createButton(post, caption, imageURLs, postId) { + post.style.position = "relative"; + + const button = document.createElement("button"); + button.textContent = "Check"; + button.setAttribute('data-post-id', postId); + button.innerHTML=svgCode + + Object.assign(button.style, { + position: "absolute", + right: "5px", + top: "-34px", + border: "none", + borderRadius: "8px", + cursor: "pointer", + zIndex: 9999, + pointerEvents: "auto", + transition: "all 0.3s", + }); + + button.addEventListener("click", async () => { + disableAllButtons(); + button.textContent = "Processing"; + button.style.opacity = "0.5"; + button.style.color="white"; + button.style.padding="5px 5px"; + button.style.background="blue"; + collectDataImmediately(post, button, caption, imageURLs, postId); + + }); + + post.appendChild(button); + } + + // Mutation observer to handle new posts + function handleMutations(mutationsList) { + mutationsList.forEach((mutation) => { + if (mutation.type === "childList" || mutation.type === "subtree") { + document.querySelectorAll("div.m.bg-s3").forEach((post) => { + if (!post.querySelector("button")) { + collectPostData(post); + } + }); + } + }); + } + + const observer = new MutationObserver(handleMutations); + observer.observe(document.querySelector(".m"), { + childList: true, + subtree: true, + }); + + document.addEventListener("DOMContentLoaded", () => { + document.querySelectorAll("div.m.displayed.bg-s3").forEach((post) => { + if (!post.dataset.processed) { + collectPostData(post); + } + }); + }); + + return true; + })(); + `; return ( - - + - - {/* Custom Navbar */} - - - router.back()} - > - - - - - - 30 Second - - News - - - - - - - - - {/* Loading Indicator */} - {loading ? ( - - - - Loading news... - - - ) : error ? ( - renderErrorView() - ) : newsData.length === 0 ? ( - - - - No News Found - - - We couldn't find any news articles. Please try again later. - - - Refresh - - - ) : ( - /* Show all news items with ads after every 4 items */ - item.id.toString()} - pagingEnabled - showsVerticalScrollIndicator={false} - snapToAlignment="start" - decelerationRate="fast" - snapToInterval={height} - /> - )} - - {/* News Detail Modal with text X close button */} - - - - {/* Text X close button instead of icon */} - - X - - - {/* Modal Content - Title and Description */} - {selectedNews && ( - - {/* Title */} - {selectedNews.title} - - {/* Date */} - - {formatDate(selectedNews.created_at)} - - - {/* All paragraphs */} - {selectedNews.paragraphs.map((paragraph, index) => ( - - {paragraph} - - ))} - - {/* Extra padding at the bottom for better scrolling */} - - - )} - - - ); } @@ -534,248 +400,7 @@ export default function NewsScreen(): JSX.Element { const styles = StyleSheet.create({ container: { flex: 1, + marginTop: 5, }, - navbar: { - flexDirection: "row", - alignItems: "center", - justifyContent: "space-between", - paddingHorizontal: 16, - }, - backButton: { - padding: 8, - borderRadius: 20, - }, - brandContainer: { - flexDirection: "row", - alignItems: "center", - }, - brandText: { - fontSize: 20, - fontWeight: "700", - }, - brandAccent: { - fontSize: 20, - fontWeight: "700", - color: "#007AFF", - marginLeft: 4, - }, - refreshButton: { - padding: 8, - borderRadius: 20, - }, - loadingContainer: { - flex: 1, - justifyContent: "center", - alignItems: "center", - }, - loadingText: { - marginTop: 10, - fontSize: 16, - color: "#007AFF", - }, - errorContainer: { - flex: 1, - justifyContent: "center", - alignItems: "center", - padding: 20, - }, - errorTitle: { - fontSize: 20, - fontWeight: "bold", - marginTop: 16, - marginBottom: 8, - }, - errorMessage: { - fontSize: 16, - textAlign: "center", - marginBottom: 20, - color: "#666", - }, - retryButton: { - backgroundColor: "#007AFF", - paddingHorizontal: 20, - paddingVertical: 10, - borderRadius: 8, - }, - retryButtonText: { - color: "#fff", - fontWeight: "600", - fontSize: 16, - }, - newsItemContainer: { - height: height, - width: width, - position: "relative", - }, - imageContainer: { - height: height * 0.3, // 30% of screen height for image - width: width, - overflow: "hidden", - }, - newsImage: { - width: "100%", - height: "100%", - }, - contentSection: { - height: height * 0.7, // 70% of screen height for content - width: width, - }, - blurContainer: { - flex: 1, - borderTopLeftRadius: 0, - borderTopRightRadius: 0, - overflow: "hidden", - }, - androidBlur: { - backgroundColor: "rgba(167, 61, 228, 0.75)", // Purple background from the provided code - }, - scrollableContent: { - flex: 1, - }, - textContainer: { - padding: 20, - paddingTop: 20, - paddingBottom: 30, - }, - date: { - color: "#ccc", - fontSize: 12, - marginBottom: 10, - }, - title: { - color: "#fff", - fontSize: 18, - fontWeight: "bold", - marginBottom: 15, - }, - paragraph: { - color: "#fff", - fontSize: 15, - lineHeight: 22, - marginBottom: 15, - }, - // Read more button styles - readMoreContainer: { - flexDirection: "row", - justifyContent: "flex-end", - marginTop: 20, - }, - readMoreButton: { - flexDirection: "row", - alignItems: "center", - backgroundColor: "rgba(255, 255, 255, 0.2)", - paddingVertical: 10, - paddingHorizontal: 16, - borderRadius: 8, - }, - readMoreText: { - color: "#ffffff", - fontWeight: "600", - fontSize: 16, - marginRight: 5, - }, - modalContainer: { - flex: 1, - backgroundColor: "rgba(0,0,0,0.5)", - }, - modalContent: { - flex: 1, - marginTop: 60, // Space from top - marginBottom: 20, // Space from bottom - marginHorizontal: 15, // Space from sides - backgroundColor: "rgba(167, 61, 228, 0.95)", // Purple background - borderRadius: 20, - overflow: "hidden", - }, - modalScrollView: { - flex: 1, - }, - modalScrollViewContent: { - padding: 20, - paddingTop: 25, - }, - modalNewsTitle: { - fontSize: 24, - fontWeight: "bold", - marginBottom: 10, - color: "#ffffff", - marginTop: 15, // Add space at the top for the close button - }, - modalDate: { - fontSize: 14, - color: "rgba(255, 255, 255, 0.7)", - marginBottom: 20, - }, - modalParagraph: { - fontSize: 17, - lineHeight: 24, - marginBottom: 16, - color: "#ffffff", - }, - closeIconButton: { - position: "absolute", - top: 15, - right: 15, - zIndex: 10, - width: 36, - height: 36, - borderRadius: 18, // Half of width/height - backgroundColor: "rgba(255, 255, 255, 0.3)", - justifyContent: "center", - alignItems: "center", - }, - closeButtonText: { - color: "#ffffff", - fontSize: 20, - fontWeight: "bold", - }, - modalBottomPadding: { - height: 40, // Extra padding at the bottom for better scrolling - }, - fullScreenItem: { - height: height, - width: width, - backgroundColor: "#ffffff", - }, - adHeader: { - backgroundColor: "#007AFF", - paddingVertical: 6, - paddingHorizontal: 12, - alignItems: "center", - }, - adLabel: { - fontSize: 12, - fontWeight: "bold", - color: "#fff", - }, - // Full screen ad styles - fullScreenAdContainer: { - height: height, - width: width, - }, - adImageContainer: { - height: height * 0.5, // 70% for image - width: width, - }, - singleAdImage: { - width: "100%", - height: "100%", - }, - singleAdContent: { - height: height * 0.5, // 30% for content - width: width, - padding: 20, - backgroundColor: "#ffffff", - }, - singleAdTitle: { - fontSize: 24, - fontWeight: "bold", - marginBottom: 15, - color: "#000", - }, - singleAdDescription: { - fontSize: 16, - lineHeight: 24, - color: "#333", - }, -}); \ No newline at end of file +}); + diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 14224c9..95f3a93 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,398 +1,531 @@ -import { WebView } from "react-native-webview"; -import { useRef, useState } from "react"; -import { Alert, StyleSheet, View } from "react-native"; -export default function HomeScreen() { - const webviewRef = useRef(null); - const [postData, setPostData] = useState([]); - const [isApiCallInProgress, setIsApiCallInProgress] = useState(false); +import { + View, + Text, + Image, + StyleSheet, + Dimensions, + Platform, + StatusBar, + FlatList, + TouchableOpacity, + SafeAreaView, + Modal, + ScrollView, + ActivityIndicator, + Linking, +} from "react-native"; +import { useState, useEffect } from "react"; +import { BlurView } from "expo-blur"; +import { useRouter } from "expo-router"; +import { IconSymbol } from "@/components/ui/IconSymbol"; +import { useColorScheme } from "@/hooks/useColorScheme"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; + +const { width, height } = Dimensions.get("window"); + +// API endpoint - use your own IP address here +const API_URL = "https://dev.dashboard.planpostai.com/news/api/news"; + +// TypeScript interfaces +interface Paragraph { + content: string; +} + +interface NewsItem { + id: number; + created_at: string; + title: string; + description: string; + image_url: string; + image_path?: string; + paragraphs: string[]; +} + +interface AdItem { + id: string; + isAd: boolean; + adIndex: number; +} + +type FeedItem = NewsItem | AdItem; + +// Type guard to check if an item is an ad +const isAd = (item: FeedItem): item is AdItem => { + return (item as AdItem).isAd === true; +}; + +// Single ad data +const singleAd = { + id: 1, + title: "Internet Packages", + description: + "Get the latest data offers...", + image: + "https://cdn01da.grameenphone.com/sites/default/files/2023-09/GP_013_New_Number_Series-1060-x-764.jpg", + url: "https://www.premium-headphones.com", +}; + +export default function NewsScreen(): JSX.Element { + const colorScheme = useColorScheme(); + const router = useRouter(); + const insets = useSafeAreaInsets(); + const [selectedNews, setSelectedNews] = useState(null); + const [modalVisible, setModalVisible] = useState(false); + const [newsData, setNewsData] = useState([]); + const [processedData, setProcessedData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Fetch news data from API + useEffect(() => { + fetchNews(); + }, []); + + // Process news data to add ads after every 4 news items + useEffect(() => { + const processed: FeedItem[] = []; + + newsData.forEach((item, index) => { + processed.push(item); + if ((index + 1) % 4 === 0 && index !== newsData.length - 1) { + processed.push({ + id: `ad-${Math.floor(index / 4)}`, + isAd: true, + adIndex: Math.floor(index / 4), + }); + } + }); + + setProcessedData(processed); + }, [newsData]); + + const fetchNews = async (): Promise => { + setLoading(true); + setError(null); - // Function to call the fact-check API - const checkFacts = async (caption, imageUrl) => { try { + // Set a timeout to prevent hanging requests + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); - const response = await fetch('https://factcheck.planpostai.com/check-facts', { - method: 'POST', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ query: caption, url:imageUrl }), + const response = await fetch(API_URL, { + signal: controller.signal, }); - const responseText = await response.text(); - console.log('Raw API Response:', responseText); + clearTimeout(timeoutId); if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status} - ${responseText}`); + throw new Error(`HTTP error! Status: ${response.status}`); } - const result = JSON.parse(responseText); - let color; - if (result?.verdict === 'True') { - color = "#2E7D32"; - } else if (result?.verdict === 'False') { - color = "#D32F2F"; - } else { - color = "#F9A825"; - } + const responseJson = await response.json(); - // Send the result back to WebView to show toast - webviewRef.current?.injectJavaScript(` - (function() { - showToast('${result.evidence.replace(/'/g, "\\'")}', 10000, '${color}'); - return true; - })(); - `); + // Extract data from the nested structure + const data = responseJson.data || responseJson; - return result; - } catch (error) { - console.error('API Error Details:', { - message: error.message, - stack: error.stack, - name: error.name - }); + if (!data || !Array.isArray(data)) { + throw new Error("Invalid data format received from API"); + } - // Show error in WebView - webviewRef.current?.injectJavaScript(` - (function() { - showToast('Error: ${error.message.replace(/'/g, "\\'")}', 5000, true); - return true; - })(); - `); + // Process the data to ensure it has the required format + const processedData: NewsItem[] = data.map((item: any) => ({ + ...item, + // If paragraphs don't exist, create them from description + paragraphs: item.paragraphs || [ + item.description.substring(0, item.description.length / 3), + item.description.substring( + item.description.length / 3, + (2 * item.description.length) / 3 + ), + item.description.substring((2 * item.description.length) / 3), + ], + })); - throw error; + setNewsData(processedData); + } catch (err) { + console.error("Failed to fetch news:", err); + setError( + err instanceof Error ? err.message : "An unknown error occurred" + ); + setNewsData([]); + } finally { + setLoading(false); } }; - const handleMessage = async (event) => { - try { - const data = JSON.parse(event.nativeEvent.data); - switch (data.type) { - case 'postData': - setPostData(prevData => [...prevData, { - caption: data.caption, - imageUrls: data.imageUrls, - postId: data.postId - }]); - - if (isApiCallInProgress) { - return; - } - - setIsApiCallInProgress(true); - - try { - const result = await checkFacts(data.caption, data.imageUrls[0]); - - setIsApiCallInProgress(false); - webviewRef.current?.injectJavaScript(` - (function() { - enableAllButtons(); - const button = document.querySelector('button[data-post-id="${data.postId}"]'); - if (button) { - button.textContent = 'Processed'; - button.style.background="green"; - button.style.color="white"; - button.style.padding="5px 5px"; - button.style.opacity = '0.7'; - button.disabled = true; - } - return true; - })(); - `); - } catch (error) { - setIsApiCallInProgress(false); - webviewRef.current?.injectJavaScript(` - (function() { - const svgCode=\` - - - - - \`; - enableAllButtons(); - const button = document.querySelector('button[data-post-id="${data.postId}"]'); - if (button) { - button.disabled = false; - button.style.opacity = '1'; - button.innerHTML = svgCode; - } - return true; - })(); - `); - } finally { - setIsApiCallInProgress(false); - } - break; - - case 'error': - console.error(`WebView Error: ${data.message}`); - break; - } - } catch (error) { - console.error('Error handling message:', error); - } + const formatDate = (dateString: string): string => { + const date = new Date(dateString); + return date.toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); }; - const injectedJavaScript = ` - (function() { - // Add toast CSS - const style = document.createElement('style'); - style.innerHTML = \` - #toast-container { - position: fixed; - top: 50px; - right: 20px; - left: 20px; - z-index: 999999; - display: flex; - flex-direction: column; - gap: 10px; - pointer-events: auto; - } - .toast { - background-color: #1db146; - color: #fff; - font-weight: 800; - padding: 12px 16px; - border-radius: 8px; - box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); - font-size: 16px; - opacity: 1; - position: relative; - width: 90%; - z-index: 99999999999; - transition: opacity 0.5s ease; - } - .toast.fade-out { - opacity: 0; - } - .error-toast { - background-color: #ff4444; - } - \`; - document.head.appendChild(style); + // Function to truncate text to 99% and add ellipsis + const truncateDescription = (text: string): string => { + const truncateLength = Math.floor(text.length * 0.99); + return text.substring(0, truncateLength) + "..."; + }; - // Function to disable all buttons - window.disableAllButtons = function() { - document.querySelectorAll("button").forEach((btn) => { - btn.disabled = true; - btn.style.opacity = "0.5"; - btn.style.cursor = "not-allowed"; - }); - }; + const openNewsModal = (news: NewsItem): void => { + setSelectedNews(news); + setModalVisible(true); + }; - // Function to enable all buttons - window.enableAllButtons = function() { - document.querySelectorAll("button").forEach((btn) => { - if (btn.textContent !== "Processed") { - btn.disabled = false; - btn.style.opacity = "1"; - btn.style.cursor = "pointer"; - } - }); - }; + const closeNewsModal = (): void => { + setModalVisible(false); + }; - // Toast notification function - window.showToast = function showToast(message, duration, color = '#333') { - // Create the toast element - const toast = document.createElement('div'); - toast.textContent = message; + // Navigate to ad website + const navigateToAdPage = (url: string): void => { + // Open the URL in the device's browser + Linking.openURL(url).catch((err) => + console.error("Couldn't open URL: ", err) + ); + }; - // Apply basic styles - toast.style.position = 'fixed'; - toast.style.top = '5px'; - toast.style.left = '50%'; - toast.style.fontSize="16px"; - toast.style.fontWeight="800"; - toast.style.transform = 'translateX(-50%)'; - toast.style.padding = '10px 20px'; - toast.style.color = '#fff'; - toast.style.backgroundColor = color; // Dynamic background color - toast.style.borderRadius = '8px'; - toast.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.3)'; - toast.style.zIndex = '9999'; - toast.style.opacity = '1'; - toast.style.transition = 'opacity 0.5s ease-in-out'; - toast.style.width = '85%'; + // Advertisement component with full screen layout + const AdvertisementCard = ({ index }: { index: number }): JSX.Element => { + return ( + + + SPONSORED + - // Add toast to the DOM - document.body.appendChild(toast); + {/* Full screen ad with vertical layout */} + navigateToAdPage(singleAd.url)} + activeOpacity={0.9} + > + {/* Image first */} + + + - // Auto-remove the toast after the specified duration - setTimeout(() => { - toast.style.opacity = '0'; // Fade out - setTimeout(() => { - document.body.removeChild(toast); // Remove from DOM - }, 500); // Match fade-out duration - }, duration); - }; + {/* Content below image */} + + {singleAd.title} + + {singleAd.description} + + + + + ); + }; - // Function to handle "See More" click - async function handleSeeMoreClick(seeMoreElement, post, button, initialCaption, initialImageURLs, postId) { - return new Promise((resolve) => { - setTimeout(() => { - seeMoreElement.click(); + const renderNewsItem = ({ item }: { item: FeedItem }): JSX.Element => { + if (isAd(item)) { + return ; + } - setTimeout(() => { - const captionElement = post.querySelector( - "div.m div.m div[data-type='text'] div.native-text" - ); - fullCaption = captionElement ? captionElement.textContent.trim() : initialCaption; + // Truncate description to 99% and add ellipsis + const truncatedDescription = truncateDescription(item.paragraphs[0]); - // Send data to React Native - window.ReactNativeWebView.postMessage(JSON.stringify({ - type: 'postData', - caption: fullCaption, - imageUrls: initialImageURLs, - postId - })); + return ( + + {/* News image - 30% of screen height */} + + + - resolve(); - }, 2000); - }, 100); - }); - } + {/* Content area - 70% of screen height */} + + {Platform.OS === "ios" ? ( + + + + {formatDate(item.created_at)} - // Function to collect post data immediately - function collectDataImmediately(post, button, initialCaption, initialImageURLs, postId) { - const captionElement = post.querySelector( - "div.m div.m div[data-type='text'] div.native-text" - ); - const caption = captionElement ? captionElement.textContent.trim() : initialCaption; + {/* Title first as requested */} + {item.title} - // Send data to React Native - window.ReactNativeWebView.postMessage(JSON.stringify({ - type: 'postData', - caption, - imageUrls: initialImageURLs, - postId - })); - } + {/* Show 99% of description with ellipsis */} + {truncatedDescription} - const svgCode=\` - - - - - \` + {/* Read more button positioned on the right */} + + openNewsModal(item)} + > + Read more + + + + + + + ) : ( + + + + {formatDate(item.created_at)} + {/* Title first as requested */} + {item.title} - // Function to collect post data and send to React Native - function collectPostData(post) { - if (post.dataset.processed === "true") return; + {/* Show 99% of description with ellipsis */} + {truncatedDescription} - // Get the caption - const captionElement = post.querySelector( - "div.m div.m div[data-type='text'] div.native-text" - ); - const caption = captionElement ? captionElement.textContent.trim() : null; + {/* Read more button positioned on the right */} + + openNewsModal(item)} + > + Read more + + + + + + + )} + + + ); + }; - // Get the first image (if exists) - const singleImageElement = post.querySelector("div.m.bg-s13 img"); - const singleImageURL = singleImageElement && singleImageElement.src ? singleImageElement.src : null; - - // Collect all image elements - const imageElements = post.querySelectorAll("div.m.bg-s13 img"); - const imageURLs = Array.from(imageElements) - .filter(img => img.src) - .map(img => img.src); - - post.dataset.processed = "true"; - - // Prioritize using a single image if only one exists, otherwise use all images - let imagesToUse = []; - if (singleImageURL) { - imagesToUse = [singleImageURL]; - } else if (imageURLs.length > 0) { - imagesToUse = imageURLs; - } - - // Create button if there's a caption or at least one image - if ((caption && caption.length > 50) || imagesToUse.length > 0) { - const postId = 'post_' + Math.random().toString(36).substr(2, 9); - createButton(post, caption, imagesToUse, postId); - } - } - - - // Create check button - function createButton(post, caption, imageURLs, postId) { - post.style.position = "relative"; - - const button = document.createElement("button"); - button.textContent = "Check"; - button.setAttribute('data-post-id', postId); - button.innerHTML=svgCode - - Object.assign(button.style, { - position: "absolute", - right: "5px", - top: "-34px", - border: "none", - borderRadius: "8px", - cursor: "pointer", - zIndex: 9999, - pointerEvents: "auto", - transition: "all 0.3s", - }); - - button.addEventListener("click", async () => { - disableAllButtons(); - button.textContent = "Processing"; - button.style.opacity = "0.5"; - button.style.color="white"; - button.style.padding="5px 5px"; - button.style.background="blue"; - collectDataImmediately(post, button, caption, imageURLs, postId); - - }); - - post.appendChild(button); - } - - // Mutation observer to handle new posts - function handleMutations(mutationsList) { - mutationsList.forEach((mutation) => { - if (mutation.type === "childList" || mutation.type === "subtree") { - document.querySelectorAll("div.m.bg-s3").forEach((post) => { - if (!post.querySelector("button")) { - collectPostData(post); - } - }); - } - }); - } - - const observer = new MutationObserver(handleMutations); - observer.observe(document.querySelector(".m"), { - childList: true, - subtree: true, - }); - - document.addEventListener("DOMContentLoaded", () => { - document.querySelectorAll("div.m.displayed.bg-s3").forEach((post) => { - if (!post.dataset.processed) { - collectPostData(post); - } - }); - }); - - return true; - })(); - `; + const renderErrorView = (): JSX.Element => ( + + + + Connection Error + + + {error} + + + Retry + + + ); return ( - - + + + {/* Custom Navbar */} + + + router.back()} + > + + + + + + + + + + + + + + {/* Loading Indicator */} + {loading ? ( + + + + Loading news... + + + ) : error ? ( + renderErrorView() + ) : newsData.length === 0 ? ( + + + + No News Found + + + We couldn't find any news articles. Please try again later. + + + Refresh + + + ) : ( + /* Show all news items with ads after every 4 items */ + item.id.toString()} + pagingEnabled + showsVerticalScrollIndicator={false} + snapToAlignment="start" + decelerationRate="fast" + snapToInterval={height} + /> + )} + + {/* News Detail Modal with text X close button */} + + + + {/* Text X close button instead of icon */} + + X + + + {/* Modal Content - Title and Description */} + {selectedNews && ( + + {/* Title */} + {selectedNews.title} + + {/* Date */} + + {formatDate(selectedNews.created_at)} + + + {/* All paragraphs */} + {selectedNews.paragraphs.map((paragraph, index) => ( + + {paragraph} + + ))} + + {/* Extra padding at the bottom for better scrolling */} + + + )} + + + ); } @@ -400,6 +533,248 @@ export default function HomeScreen() { const styles = StyleSheet.create({ container: { flex: 1, - marginTop: 35, + }, + navbar: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + paddingHorizontal: 16, + }, + backButton: { + padding: 8, + borderRadius: 20, + }, + brandContainer: { + flexDirection: "row", + alignItems: "center", + }, + brandText: { + fontSize: 20, + fontWeight: "700", + }, + brandAccent: { + fontSize: 20, + fontWeight: "700", + color: "#007AFF", + marginLeft: 4, + }, + refreshButton: { + padding: 8, + borderRadius: 20, + }, + loadingContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, + loadingText: { + marginTop: 10, + fontSize: 16, + color: "#007AFF", + }, + errorContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + padding: 20, + }, + errorTitle: { + fontSize: 20, + fontWeight: "bold", + marginTop: 16, + marginBottom: 8, + }, + errorMessage: { + fontSize: 16, + textAlign: "center", + marginBottom: 20, + color: "#666", + }, + retryButton: { + backgroundColor: "#007AFF", + paddingHorizontal: 20, + paddingVertical: 10, + borderRadius: 8, + }, + retryButtonText: { + color: "#fff", + fontWeight: "600", + fontSize: 16, + }, + newsItemContainer: { + height: height, + width: width, + position: "relative", + }, + imageContainer: { + height: height * 0.3, // 30% of screen height for image + width: width, + overflow: "hidden", + }, + newsImage: { + width: "100%", + height: "100%", + }, + contentSection: { + height: height * 0.7, // 70% of screen height for content + width: width, + }, + blurContainer: { + flex: 1, + borderTopLeftRadius: 0, + borderTopRightRadius: 0, + overflow: "hidden", + }, + androidBlur: { + backgroundColor: "rgba(167, 61, 228, 0.75)", // Purple background from the provided code + }, + scrollableContent: { + flex: 1, + }, + textContainer: { + padding: 20, + paddingTop: 20, + paddingBottom: 30, + }, + date: { + color: "#ccc", + fontSize: 12, + marginBottom: 10, + }, + title: { + color: "#fff", + fontSize: 18, + fontWeight: "bold", + marginBottom: 15, + }, + paragraph: { + color: "#fff", + fontSize: 15, + lineHeight: 22, + marginBottom: 15, + }, + // Read more button styles + readMoreContainer: { + flexDirection: "row", + justifyContent: "flex-end", + marginTop: 20, + }, + readMoreButton: { + flexDirection: "row", + alignItems: "center", + backgroundColor: "rgba(255, 255, 255, 0.2)", + paddingVertical: 10, + paddingHorizontal: 16, + borderRadius: 8, + }, + readMoreText: { + color: "#ffffff", + fontWeight: "600", + fontSize: 16, + marginRight: 5, + }, + modalContainer: { + flex: 1, + backgroundColor: "rgba(0,0,0,0.5)", + }, + modalContent: { + flex: 1, + marginTop: 60, // Space from top + marginBottom: 20, // Space from bottom + marginHorizontal: 15, // Space from sides + backgroundColor: "rgba(167, 61, 228, 0.95)", // Purple background + borderRadius: 20, + overflow: "hidden", + }, + modalScrollView: { + flex: 1, + }, + modalScrollViewContent: { + padding: 20, + paddingTop: 25, + }, + modalNewsTitle: { + fontSize: 24, + fontWeight: "bold", + marginBottom: 10, + color: "#ffffff", + marginTop: 15, // Add space at the top for the close button + }, + modalDate: { + fontSize: 14, + color: "rgba(255, 255, 255, 0.7)", + marginBottom: 20, + }, + modalParagraph: { + fontSize: 17, + lineHeight: 24, + marginBottom: 16, + color: "#ffffff", + }, + closeIconButton: { + position: "absolute", + top: 15, + right: 15, + zIndex: 10, + width: 36, + height: 36, + borderRadius: 18, // Half of width/height + backgroundColor: "rgba(255, 255, 255, 0.3)", + justifyContent: "center", + alignItems: "center", + }, + closeButtonText: { + color: "#ffffff", + fontSize: 20, + fontWeight: "bold", + }, + modalBottomPadding: { + height: 40, // Extra padding at the bottom for better scrolling + }, + fullScreenItem: { + height: height, + width: width, + backgroundColor: "#ffffff", + }, + adHeader: { + backgroundColor: "#007AFF", + paddingVertical: 6, + paddingHorizontal: 12, + alignItems: "center", + }, + adLabel: { + fontSize: 12, + fontWeight: "bold", + color: "#fff", + }, + // Full screen ad styles + fullScreenAdContainer: { + height: height, + width: width, + }, + adImageContainer: { + height: height * 0.5, // 70% for image + width: width, + }, + singleAdImage: { + width: "100%", + height: "100%", + }, + singleAdContent: { + height: height * 0.5, // 30% for content + width: width, + padding: 20, + backgroundColor: "#ffffff", + }, + singleAdTitle: { + fontSize: 24, + fontWeight: "bold", + marginBottom: 15, + color: "#000", + }, + singleAdDescription: { + fontSize: 16, + lineHeight: 24, + color: "#333", }, }); diff --git a/assets/images/guard.png b/assets/images/guard.png new file mode 100644 index 0000000..0f3405d Binary files /dev/null and b/assets/images/guard.png differ diff --git a/assets/images/news.jpeg b/assets/images/news.jpeg deleted file mode 100644 index c40c0fc..0000000 Binary files a/assets/images/news.jpeg and /dev/null differ diff --git a/assets/images/news.png b/assets/images/news.png new file mode 100644 index 0000000..26f2f73 Binary files /dev/null and b/assets/images/news.png differ diff --git a/assets/images/news_logo.png b/assets/images/news_logo.png new file mode 100644 index 0000000..d57e8e5 Binary files /dev/null and b/assets/images/news_logo.png differ diff --git a/assets/images/tabguard.png b/assets/images/tabguard.png new file mode 100644 index 0000000..b72f50e Binary files /dev/null and b/assets/images/tabguard.png differ