diff --git a/app/Events/ChatMessageSent.php b/app/Events/ChatMessageSent.php index debc56a9..f163b26d 100644 --- a/app/Events/ChatMessageSent.php +++ b/app/Events/ChatMessageSent.php @@ -31,14 +31,22 @@ public function __construct(ChatMessage $chatMessage) 'reply_to_content' => $chatMessage->reply_to_content, 'reactions' => $chatMessage->getFormattedReactions(null), 'created_at' => $chatMessage->created_at->toIso8601String(), - 'user' => [ + 'user' => $chatMessage->user ? [ 'id' => $chatMessage->user->id, 'name' => $chatMessage->user->name, 'username' => $chatMessage->user->username, 'image_url' => $chatMessage->user->image_url, 'institution' => $chatMessage->user->institution, 'is_verified' => $chatMessage->user->is_verified, - 'roles' => $chatMessage->user->roles->pluck('name')->toArray(), + 'roles' => $chatMessage->user->roles ? $chatMessage->user->roles->pluck('name')->toArray() : [], + ] : [ + 'id' => null, + 'name' => 'Deleted User', + 'username' => null, + 'image_url' => null, + 'institution' => null, + 'is_verified' => false, + 'roles' => [], ], ]; } diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index 81514adb..ce1768f1 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -12,6 +12,7 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str; use Inertia\Inertia; use Laravel\Socialite\Facades\Socialite; @@ -131,6 +132,39 @@ public function showOnboarding(Request $request) ]); } + public function checkUsername(Request $request) + { + $validator = Validator::make($request->all(), [ + 'username' => [ + 'required', + 'string', + 'min:3', + 'max:30', + 'regex:/^[a-zA-Z0-9_]+$/', + 'unique:users,username', + new CleanText, + ], + ], [ + 'username.required' => 'Please choose a username.', + 'username.min' => 'Username must be at least 3 characters.', + 'username.max' => 'Username cannot exceed 30 characters.', + 'username.regex' => "Username can only contain letters, numbers, and underscores. Dots aren't allowed.", + 'username.unique' => 'This username is already taken. Please choose another one.', + ]); + + if ($validator->fails()) { + return response()->json([ + 'available' => false, + 'message' => $validator->errors()->first('username'), + ], 422); + } + + return response()->json([ + 'available' => true, + 'message' => 'Username is available.', + ]); + } + public function completeOnboarding(Request $request) { if (Auth::check()) { diff --git a/app/Http/Controllers/ChatController.php b/app/Http/Controllers/ChatController.php index 5be50636..09cf4d41 100644 --- a/app/Http/Controllers/ChatController.php +++ b/app/Http/Controllers/ChatController.php @@ -74,7 +74,7 @@ public function index(Request $request) 'reply_to_content' => $msg->deleted_at ? null : $msg->reply_to_content, 'reactions' => $msg->getFormattedReactions($user?->id), 'created_at' => $msg->created_at->toIso8601String(), - 'user' => [ + 'user' => $msg->user ? [ 'id' => $msg->user->id, 'name' => $msg->user->name, 'username' => $msg->user->username, @@ -82,6 +82,14 @@ public function index(Request $request) 'institution' => $msg->user->institution, 'is_verified' => $msg->user->is_verified, 'roles' => $msg->user->roles->pluck('name')->toArray(), + ] : [ + 'id' => null, + 'name' => 'Deleted User', + 'username' => null, + 'image_url' => null, + 'institution' => null, + 'is_verified' => false, + 'roles' => [], ], ]); diff --git a/database/migrations/2026_09_24_214500_make_user_id_nullable_on_chat_messages_table.php b/database/migrations/2026_09_24_214500_make_user_id_nullable_on_chat_messages_table.php new file mode 100644 index 00000000..5c60287f --- /dev/null +++ b/database/migrations/2026_09_24_214500_make_user_id_nullable_on_chat_messages_table.php @@ -0,0 +1,36 @@ +dropForeign(['user_id']); + $table->foreignId('user_id')->nullable()->change(); + $table->foreign('user_id')->references('id')->on('users')->nullOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // Delete orphaned messages without an author before restoring NOT NULL + DB::table('chat_messages')->whereNull('user_id')->delete(); + + Schema::table('chat_messages', function (Blueprint $table) { + $table->dropForeign(['user_id']); + $table->foreignId('user_id')->nullable(false)->change(); + $table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete(); + }); + } +}; diff --git a/resources/js/components/navigation/Navigation.tsx b/resources/js/components/navigation/Navigation.tsx index 60ac61e6..6fa5cd0b 100644 --- a/resources/js/components/navigation/Navigation.tsx +++ b/resources/js/components/navigation/Navigation.tsx @@ -841,6 +841,9 @@ export const SiteBottomNav = defineComponent({ name: 'SiteBottomNav', setup() { const page = usePage(); + const user = computed( + () => page.props.auth?.user as AuthedUser | undefined, + ); const currentUrl = computed(() => String(page.url)); const bottomNavItems = computed(() => primaryNavItems.filter((i) => i.showInBottom !== false), @@ -848,6 +851,30 @@ export const SiteBottomNav = defineComponent({ const homeHref = computed(() => preferredHomeHref(currentUrl.value)); + const authItemHref = computed(() => { + if (!user.value) { + return '/login'; + } + + return user.value.username + ? `/u/${user.value.username}` + : '/profile'; + }); + + const isAuthItemActive = computed(() => { + if (user.value) { + return ( + currentUrl.value.startsWith('/profile') || + currentUrl.value.startsWith('/u/') + ); + } + + return ( + currentUrl.value.startsWith('/login') || + currentUrl.value.startsWith('/register') + ); + }); + const isActive = (href: string, match?: (url: string) => boolean) => { if (match) { return match(currentUrl.value); @@ -899,6 +926,38 @@ export const SiteBottomNav = defineComponent({ ))} + + + + + {user.value ? 'Profile' : 'Login'} + + ); diff --git a/resources/js/pages/Chat/Index.vue b/resources/js/pages/Chat/Index.vue index bde6cb12..f7c81d11 100644 --- a/resources/js/pages/Chat/Index.vue +++ b/resources/js/pages/Chat/Index.vue @@ -20,6 +20,7 @@ import { Radio, AtSign, Users, + UserX, } from 'lucide-vue-next'; import { ref, computed, onMounted, onUnmounted, nextTick, watch } from 'vue'; import AuthModal from '@/components/AuthModal.vue'; @@ -34,9 +35,9 @@ import { getCsrfToken } from '@/lib/useCsrf'; import { formatDateDivider, formatTime } from '@/lib/useDate'; interface ChatUser { - id: number; + id: number | null; name: string; - username: string; + username: string | null; image_url: string | null; institution: string | null; is_verified: boolean; @@ -1163,9 +1164,11 @@ const submitReport = async () => { }, body: JSON.stringify({ message_id: reportingMessage.value.id, - reported_user_id: reportingMessage.value.user.id, - reported_user_name: reportingMessage.value.user.name, - reported_user_username: reportingMessage.value.user.username, + reported_user_id: reportingMessage.value.user?.id || null, + reported_user_name: + reportingMessage.value.user?.name || 'Deleted User', + reported_user_username: + reportingMessage.value.user?.username || null, message_content: reportingMessage.value.content, message_sent_at: reportingMessage.value.created_at, reason: reportReason.value, @@ -1340,6 +1343,10 @@ const isGroupedWithPrevious = (idx: number) => { return false; } + if (!prev.user?.id || !curr.user?.id) { + return false; + } + if (Number(prev.user.id) !== Number(curr.user.id)) { return false; } @@ -1358,6 +1365,10 @@ const isBanModalOpen = ref(false); const selectedUserToBan = ref(null); const openBanModal = (user: ChatUser) => { + if (!user.id) { + return; + } + closeMobileActions(); selectedUserToBan.value = { id: user.id, @@ -1598,6 +1609,7 @@ onUnmounted(() => { class="group relative flex items-start gap-2.5 rounded-xl px-2.5 py-1.5 transition-colors duration-150 sm:gap-3 sm:px-3 sm:py-2" :class="[ currentUser && + msg.user?.id && Number(currentUser.id) === Number(msg.user.id) ? 'bg-indigo-50/30 dark:bg-indigo-950/15' : 'hover:bg-slate-50/80 dark:hover:bg-zinc-800/40', @@ -1612,28 +1624,37 @@ onUnmounted(() => { >
- - - - {{ msg.user.name?.charAt(0) || 'U' }} - - + {
- {{ msg.user.name }} + {{ + msg.user.name + }} + + + {{ msg.user?.name || 'Deleted User' }} + You - - @{{ msg.user.username }} @@ -1874,13 +1907,14 @@ onUnmounted(() => { @@ -1891,8 +1925,9 @@ onUnmounted(() => { !msg.is_deleted && !msg.deleted_at && currentUser && - Number(currentUser.id) !== - Number(msg.user.id) + (!msg.user?.id || + Number(currentUser.id) !== + Number(msg.user.id)) " type="button" @click.stop="openReportModal(msg)" @@ -1923,6 +1958,7 @@ onUnmounted(() => { v-if=" (!msg.is_deleted && !msg.deleted_at) || ((can('manage chat') || canDelete) && + msg.user?.id && Number(currentUser?.id) !== Number(msg.user.id)) " @@ -2060,7 +2096,7 @@ onUnmounted(() => { Replying to {{ - activeReplyTo.user.name + activeReplyTo.user?.name || 'Deleted User' }}: {
-
+
@@ -694,40 +761,28 @@ const getContributorAvatar = (contributor: Contributor) => { {{ form.errors.appreciations }}

- -
-
+ Terms & Conditions + + এবং + + Privacy Policy + + -তে সম্মতি দিচ্ছেন। +

@@ -744,7 +799,7 @@ const getContributorAvatar = (contributor: Contributor) => {