diff --git a/app/chat/page.tsx b/app/chat/page.tsx index 0b609e8..40c1043 100644 --- a/app/chat/page.tsx +++ b/app/chat/page.tsx @@ -1,18 +1,98 @@ -"use client" -import { useState, useEffect, useRef } from 'react'; -import { useTheme } from 'next-themes'; -import { Textarea } from '@/components/ui/textarea'; -import { ResizablePanel, ResizablePanelGroup } from '@/components/ui/resizable'; -import { Badge } from '@/components/ui/badge'; -import { Forward, Reply, ChevronUp, ChevronDown } from 'lucide-react'; -import { toast } from 'sonner'; +"use client"; +import { useState, useEffect, useRef } from "react"; +import { useTheme } from "next-themes"; +import { Textarea } from "@/components/ui/textarea"; +import { ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; +import { Badge } from "@/components/ui/badge"; +import { Forward, Reply, ChevronUp, ChevronDown } from "lucide-react"; +import { toast } from "sonner"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, -} from '@/components/ui/tooltip'; -import { Button } from '@/components/ui/button'; +} from "@/components/ui/tooltip"; +import { Button } from "@/components/ui/button"; + +const url = "https://example.com"; // Replace with your actual URL +const API_ENDPOINT = `${url}/messages`; +const CONVERSATION_API_ENDPOINT = `${url}/conversations`; +const DISCORD_API_ENDPOINT = `${url}/create-discord-channel`; +const DISCORD_MESSAGE_API_ENDPOINT = `${url}/send-discord-message`; +const SYNC_ENDPOINT = `${url}/sync`; + +const createConversationInAPI = async (conversation: { name: string; id: number; members: string[]; tags: string[] }) => { + try { + const response = await fetch(`${CONVERSATION_API_ENDPOINT}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(conversation), + }); + + if (!response.ok) { + throw new Error(`API error: ${response.statusText}`); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error('Error creating conversation in API:', error); + toast.error('Failed to create conversation on the server.'); + } +}; + +const syncMessages = async (conversationId: number) => { + try { + const response = await fetch(`${SYNC_ENDPOINT}?conversationId=${conversationId}`); + if (!response.ok) { + throw new Error(`API error: ${response.statusText}`); + } + + const data = await response.json(); + if (!data || (Array.isArray(data) && data.length === 0)) { + console.log("No messages found. Conversation may be new."); + return []; + } + + return data; + } catch (error) { + console.error("Error syncing messages:", error); + toast.error("Failed to sync messages from the server."); + } +}; + + +type CreateChannelOptions = { + name: string; + topic?: string; + isPrivate?: boolean; + categoryId?: string; +}; + +const createDiscordChannel = async (options: CreateChannelOptions) => { + try { + const response = await fetch(DISCORD_API_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(options), + }); + + if (!response.ok) { + throw new Error(`Discord API error: ${response.statusText}`); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error('Error creating Discord channel:', error); + toast.error('Failed to create Discord channel.'); + } +}; + const convertMarkdownToHTML = (text: string) => { return text @@ -164,8 +244,15 @@ const ChatMessage = ({message, message_index, messages, reply_func, searchText, } text-sm ${isSameSideNext ? 'pb-2' : 'pb-4'} group`; const messageContentClass = `p-3 rounded-lg relative group ${ - isRight ? isSameSideNext ? '' : 'rounded-br-none' : isSameSideNext ? '' : 'rounded-bl-none' - } bg-gray-200 text-black dark:bg-[#303030] dark:text-foreground`; + isRight + ? isSameSideNext + ? "" + : "rounded-br-none" + : isSameSideNext + ? "" + : "rounded-bl-none" + } bg-black text-white`; + return (
@@ -252,6 +339,20 @@ export default function Home() { const matchElements = useRef<(HTMLElement | null)[]>([]); const [isSearching, setIsSearching] = useState(false); + useEffect(() => { + const syncData = async () => { + if (!conversationData?.id) return; // Avoid running sync if no conversationData + + const messages = await syncMessages(conversationData.id); + const updatedConversations = [...conversations]; + updatedConversations[conversationIndex].messages = messages; + setConversations(updatedConversations); + }; + + const interval = setInterval(syncData, 1500); + return () => clearInterval(interval); + }, [conversationIndex, conversations]); + useEffect(() => { if (!isSearching) { scrollToBottom(); @@ -267,19 +368,21 @@ export default function Home() { const matches: { messageIndex: number; globalMatchIndex: number }[] = []; const newMessageMatchStarts: Record = {}; - conversationData.messages.forEach((message, messageIndex) => { - newMessageMatchStarts[messageIndex] = globalMatchIndex; - - if (message instanceof Message && searchText) { - const matchArray = - message.text.match(new RegExp(escapeRegExp(searchText), 'gi')) || []; - - matchArray.forEach(() => { - matches.push({ messageIndex, globalMatchIndex }); - globalMatchIndex++; - }); - } - }); + if (conversationData?.messages) { + conversationData.messages.forEach((message, messageIndex) => { + newMessageMatchStarts[messageIndex] = globalMatchIndex; + + if (message instanceof Message && searchText) { + const matchArray = + message.text.match(new RegExp(escapeRegExp(searchText), "gi")) || []; + + matchArray.forEach(() => { + globalMatchIndex++; + }); + } + }); + } + setMessageMatchStarts(newMessageMatchStarts); setTotalMatches(globalMatchIndex); @@ -349,44 +452,55 @@ export default function Home() { if (scrollAreaRef.current) { scrollAreaRef.current.scrollTo({ top: scrollAreaRef.current.scrollHeight, - behavior: 'smooth', + behavior: "smooth", }); } }; - const sendMessage = (text: string) => { + + + const sendMessage = async (text: string) => { const cleanedText = text.trim(); - if (cleanedText === '') { - toast.error('Message cannot be empty!'); + if (cleanedText === "") { + toast.error("Message cannot be empty!"); return; } - - const updatedMessages = [ - ...conversations[conversationIndex].messages, - new Message(conversations[conversationIndex].messages.length + 1, 'You', cleanedText, 'right', replyingTo ? replyingTo.text : undefined), - ]; - - const updatedConversation = { - ...conversations[conversationIndex], - messages: updatedMessages, - }; - - const updatedConversations = [...conversations]; - updatedConversations[conversationIndex] = updatedConversation; - - setConversations(updatedConversations); - setReplyingTo(null); - setTextboxText(''); - - setTimeout(() => { - scrollToBottom(); - }, 100); - }; - - const reply = (message_text: string, username: string) => { - setReplyingTo({ text: message_text, username }); - inputRef.current?.focus(); + + const currentConversation = conversations[conversationIndex]; + + if (!currentConversation) { + toast.error("No conversation selected."); + return; + } + + try { + await sendMessageToAPI(currentConversation.id, cleanedText); + + const messageData = { + id: (currentConversation.messages?.length || 0) + 1, + text: cleanedText, + username: "You", + timestamp: new Date().toISOString(), + }; + + const updatedMessages = [ + ...(currentConversation.messages || []), + messageData, + ]; + + const updatedConversations = [...conversations]; + updatedConversations[conversationIndex] = { + ...currentConversation, + messages: updatedMessages, + }; + + setConversations(updatedConversations); + setTextboxText(""); + } catch (error) { + console.error("Error sending message:", error); + } }; + const toggleSearchBox = () => { if (showSearchBox) { @@ -396,16 +510,91 @@ export default function Home() { } setShowSearchBox(!showSearchBox); }; - - const handleNewConversation = () => { - const newConversation = new Conversation('New Conversation', conversations.length + 1, ['You', 'Developer'], [],['Tag']); + const sendMessageToAPI = async (conversationId: number, message: string) => { + try { + const response = await fetch(`${API_ENDPOINT}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + conversationId, + message, + }), + }); + + if (!response.ok) { + throw new Error(`API error: ${response.statusText}`); + } + + const data = await response.json(); + + // Send the message to Discord as well + await sendDiscordMessage(conversationId, message); + + return data; + } catch (error) { + console.error('Error sending message to API:', error); + toast.error('Failed to send message to the server.'); + } + }; + const sendDiscordMessage = async (conversationId: number, message: string) => { + try { + const response = await fetch(DISCORD_MESSAGE_API_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + conversationId, + message, + }), + }); + + if (!response.ok) { + throw new Error(`Discord API error: ${response.statusText}`); + } + + const data = await response.json(); + return data; + } catch (error) { + console.error('Error sending message to Discord:', error); + toast.error('Failed to send message to Discord.'); + } + }; + + const handleNewConversation = async () => { + const newConversation = new Conversation( + 'New Conversation', + conversations.length + 1, + ['You', 'Developer'], + [], + ['Tag'] + ); + + await createConversationInAPI({ + name: newConversation.name, + id: newConversation.id, + members: newConversation.members, + tags: newConversation.tags, + }); + + const discordChannel = await createDiscordChannel({ + name: newConversation.name, + }); + + if (discordChannel && discordChannel.channelId) { + newConversation.id = discordChannel.channelId; + } + setConversations([...conversations, newConversation]); setConversationIndex(conversations.length); - + setTimeout(() => { scrollToBottom(); }, 50); }; + useEffect(() => { const handleGlobalKeyDown = (event: any) => { @@ -428,7 +617,7 @@ export default function Home() { }, [showSearchBox]); return ( -
+
{showSearchBox && ( )} @@ -463,25 +652,21 @@ export default function Home() {
-
-
-
-
-

- {conversationData.messages.map((message, index) => { - if (message instanceof Message) { - return ( - - ); - } else if (message instanceof ConversationEvent) { - return ; - } - return null; - })} -

-
+
+
+
+ {conversationData?.messages?.length > 0 ? ( + conversationData.messages.map((message, index) => ( +
+

+ {message.username}: {message.text} +

+
+ )) + ) : ( +

No messages to display

+ )}
-
{replyingTo && (
@@ -501,4 +686,4 @@ export default function Home() {
); -} +} \ No newline at end of file diff --git a/translations/ps.yaml b/translations/ps.yaml new file mode 100644 index 0000000..18305e7 --- /dev/null +++ b/translations/ps.yaml @@ -0,0 +1,104 @@ +Language: + name: "پښتو" + name_en: "Pashto" + iso_code: "ps" + credits: "ضیا" + version: "2.0.0" + +Translations: + + # MARK: Globals + + confirm: "تایید" + cancel: "لغوه" + 'yes': "هو" + 'no': "نه" + continue: "دوام ورکړئ" + loading: "لوډېږي..." + success: "بریا" + error: "تېروتنه" + done: "بشپړ شو!" + set: "تنظیم شوی" + setting: "تنظیمېږي..." + unknown: "نامعلوم" + download: "ډاونلوډ" + downloading: "ډاونلوډېږي {0}..." + clear: "پاک کړئ" + search: "لټون وکړئ" + submit: "سپارل" + install: "انسټال کړئ" + uninstall: "ان انسټال کړئ" + + # MARK: Backend + + main.errors_and_warnings: "په لاګ فایلونو کې تېروتنې او خبرداري:" + main.errors: "تېروتنې:" + main.warnings: "خبرداري:" + main.no_errors_or_warnings: "هیڅ تېروتنه یا خبرداری ونه موندل شو." + main.overseer_started: "ETS2LA Overseer پیل شو!" + main.development_mode: "ETS2LA د پرمختیايي حالت کې روان دی." + main.restarting: "ETS2LA بیا پیلېږي..." + main.updating: "ETS2LA تازه کېږي..." + main.update_done: "تازه کول پای ته ورسید... بیا پیلېږي!" + main.crashed: "ETS2LA د لاندې تېروتنې سره کریش شو:" + main.legacy_traceback: "د تېروتنې تریس بیک پخوانی حالت وکارول شو." + main.send_report: "پورته تریس بیک پراختیا کونکو ته واستوئ." + main.closed: "ETS2LA بند شو." + main.press_enter: "د وتلو لپاره انټر ووهئ..." + core.backend_started: "ETS2LA بیک اینډ بریالۍ پیل شو." + immediate.websocket_started: "ویب ساکټ پیل شو." + immediate.message_error: "د پیغام پروسس کولو پر مهال تېروتنه رامنځته شوه." + immediate.empty_page: "خالي پاڼه لیږل شوې وه." + webserver.enabling_plugin: "پلګین فعالېږي {0}" + webserver.disabling_plugin: "پلګین غیر فعالېږي {0}" + webserver.webserver_started: "ویب سرور په {0} ( {1} ) پیل شو" + webserver.frontend_started: "فرنټ اینډ په {0} ( {1} ) پیل شو" + godot.server_started: "ګودوت سرور د {0} پورټ باندې کار کوي" + webpage.opened: "ETS2LA UI خلاص شو." + webpage.ui_loading: "مهرباني وکړئ انتظار وکړئ، د کارونکي انٹرفېس چمتو کېږي" + controls.listener_started: "د کنټرولونو اوریدونکی فعال شو." + sounds.missing_sound: "ساؤنډپېک '{0}' کې اړین غږ '{1}' نشته." + sounds.invalid_file_type: "ساؤنډپېک '{0}' د غلط فایل ډول '{1}' لري." + sounds.sound_not_found_in_soundpack: "د '{0}' غږ غږول غوښتل، خو دا په '{1}' ساؤنډپېک کې نه و موندل شوی." + logging.delete_log_error: "د لاګ فایل حذف نشو." + logging.logger_initialized: "لاګر پیل شو." + events.vehicle_change.vehicle_change: "د موټر بدلون کشف شو." + events.vehicle_change.vehicle_change_description: "موږ نوی موټر کشف کړی. مهرباني وکړئ F4 فشار ورکړئ، د سیټ تنظیمات خلاص کړئ، او خپل نوی FOV ارزښت داخل کړئ." + + # MARK: Tutorials + + tutorials.main.window_controls: "دا د کړکۍ کنټرولونه دي. چپ اړخ بټن اضافي اختیارونه لري، دا وڅیړئ!" + tutorials.main.slide_area: "تاسو کولی شئ کړکۍ حرکت ورکړئ د دې ساحې کلیکولو او کشولو سره." + tutorials.main.sidebar_collapse: "تاسو کولی شئ سایډ بار پټ کړئ او 'بشپړ پرده' حالت ته لاړ شئ." + tutorials.main.sidebar: "تاسو کولی شئ د سایډبار له لارې د ایپ مختلف پاڼې خلاص کړئ." + tutorials.main.settings: "د ترتیب پاڼې ته لاړ شئ!" + tutorials.settings.static: "دلته هغه تنظیمات دي چې د ټول ایپ لپاره اړین دي." + tutorials.settings.global: "تاسو اوس مهال د ایپ عمومي تنظیمات ګورئ." + tutorials.settings.plugin: "دا د ټولو پلګینونو لیست دی چې د ترتیب پاڼې لري." + tutorials.settings.sdk: "د وروستي مرحلې لپاره، د SDK ترتیباتو پاڼه خلاصه کړئ او دا نصب کړئ." + + # MARK: Plugins + + plugins.adaptivecruisecontrol: "موافقه شوې کروز کنټرول" + plugins.adaptivecruisecontrol.description: "دا د مخکې موټر لپاره سرعت کموي (که د څیزونو کشف فعال وي)." + plugins.map: "نقشه" + plugins.map.description: "د نقشې کشف پلګین." + plugins.objectdetection: "د څیزونو کشف" + plugins.objectdetection.description: "کمپیوټر ویژن د څیزونو کشف لپاره کاروي." + plugins.trafficlightdetection: "د ترافیک څراغ کشف" + plugins.trafficlightdetection.description: "دا د ترافیک څراغونه کشفوي او د کروز کنټرول سیسټم سره یوځای کوي." + + # MARK: About + + about.about: "زموږ په اړه" + about.description: "ETS2LA یوه پروژه ده چې د ETS2 او ATS لپاره د اتومات چلولو آسانه حلونه برابروي." + about.statistics: "احصایې" + about.statistics.users_online: "آنلاین کارونکي:" + about.statistics.users_online_value: "{0} کارونکي" + about.statistics.past_24h: "وروستۍ 24 ساعته:" + about.statistics.past_24h_value: "{0} ځانګړي کارونکي" + about.developers: "پرمختیا کونکي / شراکت داران" + about.translation_credits: "د ژباړې کریډیټونه" + about.no_credits: "د دې ژبې لپاره هیڅ کریډیټ نشته." + about.support_development: "د پرمختګ ملاتړ وکړئ" + about.kofi_description: "• که تاسو پروژه خوښوئ، تاسو کولی شئ د Ko-Fi له لارې مرسته وکړئ." diff --git a/translations/ur.yaml b/translations/ur.yaml new file mode 100644 index 0000000..40f32d3 --- /dev/null +++ b/translations/ur.yaml @@ -0,0 +1,104 @@ +Language: + name: "اردو" + name_en: "Urdu" + iso_code: "ur" + credits: "ضياء" + version: "2.0.0" + +Translations: + + # MARK: Globals + + confirm: "تصدیق کریں" + cancel: "منسوخ کریں" + 'yes': "ہاں" + 'no': "نہیں" + continue: "جاری رکھیں" + loading: "لوڈ ہو رہا ہے..." + success: "کامیابی" + error: "غلطی" + done: "مکمل!" + set: "سیٹ" + setting: "سیٹ کیا جا رہا ہے..." + unknown: "نامعلوم" + download: "ڈاؤن لوڈ" + downloading: "ڈاؤن لوڈ ہو رہا ہے {0}..." + clear: "صاف کریں" + search: "تلاش کریں" + submit: "جمع کریں" + install: "انسٹال کریں" + uninstall: "اَن انسٹال کریں" + + # MARK: Backend + + main.errors_and_warnings: "لاگ فائل میں غلطیاں اور وارننگز:" + main.errors: "غلطیاں:" + main.warnings: "وارننگز:" + main.no_errors_or_warnings: "کوئی غلطی یا وارننگ نہیں ملی۔" + main.overseer_started: "ETS2LA Overseer شروع ہو گیا!" + main.development_mode: "ETS2LA ڈویلپمنٹ موڈ میں چل رہا ہے۔" + main.restarting: "ETS2LA دوبارہ شروع ہو رہا ہے..." + main.updating: "ETS2LA اپ ڈیٹ ہو رہا ہے..." + main.update_done: "اپ ڈیٹ مکمل... دوبارہ شروع ہو رہا ہے!" + main.crashed: "ETS2LA مندرجہ ذیل غلطی کے ساتھ کریش ہو گیا:" + main.legacy_traceback: "پرانا ٹریس بیک استعمال کیا گیا۔" + main.send_report: "اوپر دیا گیا ٹریس بیک ڈیولپرز کو بھیجیں۔" + main.closed: "ETS2LA بند ہو گیا۔" + main.press_enter: "باہر نکلنے کے لیے انٹر دبائیں..." + core.backend_started: "ETS2LA بیک اینڈ کامیابی سے شروع ہو گیا۔" + immediate.websocket_started: "ویب ساکٹ شروع ہو گیا۔" + immediate.message_error: "پیغام پراسیس کرنے میں غلطی ہوئی۔" + immediate.empty_page: "خالی صفحہ بھیجنے کی کوشش کی گئی۔" + webserver.enabling_plugin: "پلگ ان فعال کیا جا رہا ہے {0}" + webserver.disabling_plugin: "پلگ ان غیر فعال کیا جا رہا ہے {0}" + webserver.webserver_started: "ویب سرور {0} ( {1} ) پر شروع ہو گیا" + webserver.frontend_started: "فرنٹ اینڈ {0} ( {1} ) پر شروع ہو گیا" + godot.server_started: "گوڈوٹ {0} پورٹ پر سروس دے رہا ہے" + webpage.opened: "ETS2LA UI کھل گیا۔" + webpage.ui_loading: "براہ کرم انتظار کریں، یوزر انٹرفیس تیار ہو رہا ہے" + controls.listener_started: "کنٹرول لسٹنر شروع ہو گیا۔" + sounds.missing_sound: "ساؤنڈ پیک '{0}' میں مطلوبہ آواز '{1}' غائب ہے۔" + sounds.invalid_file_type: "ساؤنڈ پیک '{0}' میں غیر موزوں فائل ٹائپ '{1}' ہے۔" + sounds.sound_not_found_in_soundpack: "آواز '{0}' چلانے کی کوشش کی گئی، لیکن یہ ساؤنڈ پیک '{1}' میں نہیں ملی۔" + logging.delete_log_error: "لاگ فائل کو حذف نہیں کیا جا سکا۔" + logging.logger_initialized: "لاگر شروع ہو گیا۔" + events.vehicle_change.vehicle_change: "گاڑی کی تبدیلی کا پتہ چلا۔" + events.vehicle_change.vehicle_change_description: "ہم نے نئی گاڑی کا پتہ لگا لیا ہے۔ براہ کرم کیبن ویو میں F4 دبائیں، سیٹ کی ترتیبات کھولیں، اور اپنا نیا FOV ویلیو درج کریں۔" + + # MARK: Tutorials + + tutorials.main.window_controls: "یہ ونڈو کنٹرول ہیں۔ سب سے بائیں بٹن اضافی آپشنز کے لیے ہے۔ اس پر ماؤس رکھ کر تفصیل دیکھیں!" + tutorials.main.slide_area: "آپ ونڈو کو اس علاقے پر کلک کرکے اور گھسیٹ کر حرکت دے سکتے ہیں۔" + tutorials.main.sidebar_collapse: "آپ سائڈبار کو دبانے سے بند کر سکتے ہیں اور 'فل سکرین' موڈ میں جا سکتے ہیں۔" + tutorials.main.sidebar: "آپ سائڈبار کے ذریعے ایپ کے مختلف صفحات کو آسانی سے نیویگیٹ کر سکتے ہیں۔" + tutorials.main.settings: "فی الحال، آپ کو ترتیبات کے صفحے پر جانا چاہیے!" + tutorials.settings.static: "یہ وہ ترتیبات ہیں جو پوری ایپ پر اثر انداز ہوتی ہیں۔" + tutorials.settings.global: "یہاں آپ عالمی ترتیبات دیکھ سکتے ہیں، جن میں UI کے اختیارات شامل ہیں۔" + tutorials.settings.plugin: "یہ تمام پلگ انز کی فہرست ہے جن کی اپنی ترتیبات ہیں۔" + tutorials.settings.sdk: "آخری مرحلے میں، SDK سیٹنگز کو کھولیں اور انسٹال کریں، اگر آپ نے پہلے سے نہیں کیا۔" + + # MARK: Plugins + + plugins.adaptivecruisecontrol: "ایڈاپٹیو کروز کنٹرول" + plugins.adaptivecruisecontrol.description: "یہ آگے موجود گاڑی کے مطابق رفتار کو کم کرے گا (اگر آبجیکٹ ڈیٹیکشن ماڈل فعال ہے)۔" + plugins.map: "نقشہ" + plugins.map.description: "نقشے کی شناخت کا پلگ ان۔" + plugins.objectdetection: "آبجیکٹ ڈیٹیکشن" + plugins.objectdetection.description: "کمپیوٹر وژن کی مدد سے اشیاء کی شناخت کرتا ہے۔" + plugins.trafficlightdetection: "ٹریفک سگنل ڈیٹیکشن" + plugins.trafficlightdetection.description: "ٹریفک لائٹس کی شناخت کر کے کروز کنٹرول سسٹم سے ہم آہنگ کرتا ہے۔" + + # MARK: About + + about.about: "ہمارے بارے میں" + about.description: "ETS2LA ایک پروجیکٹ ہے جو ETS2 اور ATS کے لیے ایک خودکار ڈرائیونگ حل فراہم کرتا ہے۔" + about.statistics: "اعداد و شمار" + about.statistics.users_online: "آن لائن صارفین:" + about.statistics.users_online_value: "{0} صارفین" + about.statistics.past_24h: "پچھلے 24 گھنٹوں میں:" + about.statistics.past_24h_value: "{0} منفرد صارفین" + about.developers: "ڈویلپرز / شراکت دار" + about.translation_credits: "ترجمہ کا شکریہ" + about.no_credits: "اس زبان کے لیے کوئی کریڈٹ دستیاب نہیں۔" + about.support_development: "ترقی کی حمایت کریں" + about.kofi_description: "• اگر آپ کو یہ پروجیکٹ پسند آیا اور آپ اس کی مدد کرنا چاہتے ہیں، تو آپ Ko-Fi کے ذریعے عطیہ کر سکتے ہیں۔"