-
-
Notifications
You must be signed in to change notification settings - Fork 313
Feat: Enable chat requests to users who haven’t unlocked their chats #5313
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
👋 Hi @e-esakman! This pull request needs a peer review before it can be merged. Please request a review from a team member who is not:
Once a valid peer review is submitted, this check will pass automatically. Thank you! |
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the WalkthroughAdds ChatRequest model and migration, adds slug and timezone to UserProfile with save logic, introduces Redis-backed rate limiting and a send_chat_request endpoint, updates start-thread flow for locked recipients, adjusts search UI/JS to request chats, adds route and tests. Changes
Sequence Diagram(s)sequenceDiagram
participant Browser as Sender (browser)
participant UI as Search UI (client JS)
participant App as Django view (send_chat_request / start_thread)
participant Redis as Redis (rate limiter)
participant DB as Database (ChatRequest / Thread)
participant Email as Email service
Browser->>UI: Click "Request Chat" / "Message"
UI->>App: POST /chat/request/{receiver_id} or /messaging/start-thread/{userId}/ (with CSRF)
App->>Redis: check/increment rate limit for sender->receiver
alt rate limit exceeded
Redis-->>App: limit reached
App-->>UI: 429 / {"error":"rate_limit"}
UI->>Browser: show error / revert button
else rate OK
App->>DB: lookup existing ChatRequest
alt existing record and locked
DB-->>App: ChatRequest exists (locked/unlocked)
App-->>UI: {"locked": true, "email_sent": ...}
else create new ChatRequest
App->>DB: create ChatRequest (sender, receiver)
DB-->>App: ChatRequest created
App->>Email: send unlock/notification email (if needed)
Email-->>App: delivery status
App->>DB: mark email_sent = true (if sent)
App-->>UI: {"locked": true, "email_sent": true} or {"success": true, "thread_id": id}
UI->>Browser: update button state or redirect to thread
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Suggested labels
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
📊 Monthly LeaderboardHi @e-esakman! Here's how you rank for December 2025:
Leaderboard based on contributions in December 2025. Keep up the great work! 🚀 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (3)
website/views/user.py (1)
1719-1794: start_thread flow looks correct; consider tightening a couple of edgesThe locked/unlocked split, single-email behaviour via
email_sent, and thread creation all look consistent with the new ChatRequest model and the front-end expectations.Two non-blocking tweaks you may want to consider:
- Add a cheap guard against self-chat to avoid weird UX like emailing a user to unlock their own chat with themselves:
@login_required def start_thread(request, user_id): if request.method != "POST": return JsonResponse({"success": False, "error": "Invalid request"}, status=400) current_user = request.user other_user = get_object_or_404(User, id=user_id) + + if other_user == current_user: + return JsonResponse({"success": False, "error": "Cannot start a thread with yourself"}, status=400)
- If there’s any chance
other_user.userprofilemight be missing, you could defensively handleUserProfile.DoesNotExistinstead of relying on thegetattrshortcut; this is in line with other assumptions in this file though, so treat it as optional.website/views/core.py (1)
49-77: User search + pending-request wiring is sound; small UX gaps you may want to closeThe changes to
search()correctly:
- Load users with their
UserProfileand scores.- Compute
sent_requestsviaChatRequestfor the current user.- Attach
has_pending_requestandbadgesto each user object for thetype='users'view, matching the template’s expectations.Two minor UX details to consider (not blockers):
type='all'search doesn’t reflect pending requestsFor the
"all"search path,usersis a plain queryset ofUserwithout thehas_pending_requestflag orbadgesattached. The template’s “Users” section still renders for that case and will therefore always show “Request Chat” instead of “Request Sent” even when aChatRequestalready exists.If you want consistent behaviour between
"all"and"users"searches, you could reuse the samesent_requests/users_listconstruction for the"all"case (or refactor into a helper used by both paths).Distinguishing “request exists but no email was sent” (optional)
has_pending_requestis currently keyed purely on the presence of aChatRequest. In the rare case where a request was created whilereceiver.emailwas empty, the UI will still show “Request Sent” later even though no email was ever delivered. If that distinction matters, you might drive the “Request Sent” label fromemail_sent(and potentially allow re-request ifemail_sentis still false).Overall the logic is correct; these are UX-level refinements you can choose to defer.
Also applies to: 603-669
website/templates/search.html (1)
322-340: Unified Message / Request Chat UI matches backend; only nit is some dead class handlingThe template + JS wiring lines up nicely with the new
start_threadJSON API:
- Correctly shows:
- “Message” when
user.userprofile.public_keyexists.- Disabled “Request Sent” when
user.has_pending_requestis true.- “Request Chat” otherwise, using the same POST endpoint and relying on
data.lockedvsdata.successto decide between “Request Sent” and redirect.- CSRF handling via
getCookie("csrftoken")and POST is appropriate for this view.One tiny clean-up you may want to make:
In the locked branch you call:
this.classList.remove("bg-[#e74c3c]", "hover:bg-[#c0392b]"); this.classList.add("bg-red-500", "cursor-not-allowed");but none of the current buttons use
bg-[#e74c3c]orhover:bg-[#c0392b]anymore, so those removes are no-ops. You could either update the removals to match the current grey/indigo classes (if you want to normalise the visual state) or just drop theclassList.removecall entirely to reduce noise.Behaviour-wise everything is solid.
Also applies to: 785-848
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting
📒 Files selected for processing (5)
website/migrations/0264_add_slug_timezone_and_chatrequest.py(1 hunks)website/models.py(4 hunks)website/templates/search.html(2 hunks)website/views/core.py(3 hunks)website/views/user.py(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
website/views/user.py (2)
website/models.py (2)
ChatRequest(3089-3103)Thread(3064-3069)website/static/js/invite.js (1)
subject(75-75)
website/views/core.py (1)
website/models.py (2)
ChatRequest(3089-3103)UserBadge(1682-1696)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Run Tests
- GitHub Check: docker-test
🔇 Additional comments (3)
website/models.py (3)
906-906: LGTM!The slug field definition is appropriate. Using
unique=Truewithnull=Trueis valid in Django since NULL values are excluded from uniqueness checks.
935-935: LGTM!The timezone field with
max_length=50and default"UTC"is appropriate for storing timezone identifiers.
3089-3103: LGTM!The
ChatRequestmodel is well-designed for the chat request feature:
- Appropriate use of
CASCADEon delete for user referencesunique_togetherconstraint correctly prevents duplicate requests between the same users- Clear status tracking with
email_sentandis_unlockedfields
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (1)
website/models.py (1)
909-969: UserProfile.slug should be slugified and collision-safe (issue already raised previously)
UserProfile.slugis aSlugField(unique=True, …), butsave()assignsself.user.usernamedirectly:if not self.slug: self.slug = self.user.usernameDjango usernames can contain characters (
@,.,+, etc.) that are invalid forSlugFieldvalidators and URL slug converters, and this also doesn’t handle collisions if you later derive slugs differently. This matches the concern in the earlier review comment that suggested usingslugify()instead of raw username.A safer implementation would mirror the slug logic used on
Organization/Repo:class UserProfile(models.Model): @@ - slug = models.SlugField(unique=True, blank=True, null=True) + slug = models.SlugField(unique=True, blank=True, null=True) @@ - def save(self, *args, **kwargs): - if not self.slug: - self.slug = self.user.username - super().save(*args, **kwargs) + def save(self, *args, **kwargs): + if not self.slug and self.user_id: + # Start from a slugified username + base_slug = slugify(self.user.username) + if not base_slug: + base_slug = f"user-{self.user_id}" + + self.slug = base_slug + counter = 1 + while UserProfile.objects.filter(slug=self.slug).exclude(pk=self.pk).exists(): + self.slug = f"{base_slug}-{counter}" + counter += 1 + + super().save(*args, **kwargs)This preserves existing custom slugs, ensures the value is a valid slug, and avoids collisions.
Check Django’s `SlugField` and `slugify` documentation to confirm valid characters for slugs and common patterns for generating unique slugs from usernames.
🧹 Nitpick comments (2)
website/models.py (2)
3092-3106: ChatRequest model is solid; optional CheckConstraint to prevent self-requestsThe
ChatRequestmodel structure (ForeignKeys toAUTH_USER_MODEL,unique_together = ("sender", "receiver"), and clear__str__) is well-designed.If your domain requires preventing users from requesting a chat with themselves, you can add a DB-level check constraint:
class ChatRequest(models.Model): created_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ("sender", "receiver") + constraints = [ + models.CheckConstraint( + check=~models.Q(sender=models.F("receiver")), + name="chatrequest_sender_not_receiver", + ), + ]Django 5.2.x fully supports this constraint pattern with F() expressions. Alternatively, add validation in the view/service layer if preferred.
909-909: Consider consolidating timezone handling betweenUserProfileandReminderSettingsBoth models define identical
timezone = models.CharField(max_length=50, default="UTC")fields. However,ReminderSettingshas proper validation via itssave()method (which usespytz.timezone()) and providesget_timezone_choices()returningpytz.common_timezones.UserProfile.timezonelacks this validation and integration. To avoid divergence, either reuseReminderSettings' timezone approach inUserProfileor consolidate to a single timezone management pattern across both models.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting
📒 Files selected for processing (3)
website/migrations/0264_add_slug_timezone_and_chatrequest.py(1 hunks)website/models.py(5 hunks)website/views/user.py(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- website/views/user.py
- website/migrations/0264_add_slug_timezone_and_chatrequest.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Run Tests
- GitHub Check: docker-test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (2)
website/views/core.py (2)
646-656: Restore sorting and pagination to user search query.The query is missing
.order_by("-total_score")[0:20]that was present in the original implementation. Without this:
- All matching
UserProfileobjects are fetched from the database (no limit)- Results are unsorted, making them less relevant
- This causes performance degradation, especially with many users
This issue was previously flagged in past reviews (Reference ID: 7531570).
Apply this diff to restore sorting and limiting:
elif stype == "users": users = ( UserProfile.objects.filter(user__username__icontains=query) .select_related("user") .prefetch_related( Prefetch( "user__userbadge_set", queryset=UserBadge.objects.select_related("badge"), to_attr="badges", ) ) + .order_by("-total_score")[0:20] )
902-928: Improve time-based fallback for rate limiting.The Redis fallback on line 911 only checks if any
ChatRequestexists between the two users, not whether the rate limit is actually exceeded within a time window. This means:
- If Redis is unavailable and a ChatRequest exists, no new requests are ever allowed (regardless of time)
- The fallback doesn't enforce the intended "3 per minute" limit
A previous review comment suggested implementing a time-based DB fallback (Reference ID comment about weak rate limit fallback).
Consider this time-based fallback:
try: redis_conn = get_redis_connection("default") except Exception: logger.warning("Redis unavailable for chat request rate limiting") - return ChatRequest.objects.filter(sender_id=sender_id, receiver_id=recipient_id).count() < 1 + one_minute_ago = timezone.now() - timedelta(minutes=1) + recent_sender_requests = ChatRequest.objects.filter( + sender_id=sender_id, created_at__gte=one_minute_ago + ).count() + recent_recipient_requests = ChatRequest.objects.filter( + receiver_id=recipient_id, created_at__gte=one_minute_ago + ).count() + return recent_sender_requests <= 3 and recent_recipient_requests <= 3
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting
📒 Files selected for processing (1)
website/views/core.py(4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
website/views/core.py (1)
website/models.py (4)
Badge(1667-1681)ChatRequest(3091-3105)UserProfile(893-1118)UserBadge(1684-1698)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Run pre-commit
- GitHub Check: Run Tests
- GitHub Check: docker-test
🔇 Additional comments (2)
website/views/core.py (2)
37-37: LGTM: Import additions support new chat request functionality.The added imports (
Prefetch,get_redis_connection,ChatRequest) are appropriately used for query optimization and the new chat request rate limiting feature.Also applies to: 48-48, 54-54
604-604: LGTM: Query optimization added.The
select_related("userprofile")anddistinct()additions properly optimize the user query to avoid N+1 queries while maintaining the result limit.
|
@coderabbitai The send_chat_request endpoint in core.py is intentionally limited to core domain logic only (validation, rate limiting, duplicate prevention, and ChatRequest creation). Email notifications and unlock-link generation are already handled in the existing user flow (website/views/user.py), which fixes notification behavior. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 122 🏁 Script executed: Length of output: 53 🏁 Script executed: Length of output: 2395 🏁 Script executed: Length of output: 2922 🏁 Script executed: Length of output: 2300 🏁 Script executed: Length of output: 2165 🏁 Script executed: Length of output: 2848 🏁 Script executed: Length of output: 3639 @e-esakman Thank you for explaining the architectural decision. I can see the intent to separate core domain logic (validation, rate limiting, duplicate prevention) from the user-facing flow (email notifications, thread creation). However, after reviewing the implementation, I noticed a potential issue: The Looking at the frontend code in const response = await fetch(`/messaging/start-thread/${userId}/`, {
method: "POST",
// ...
});This means:
Recommendation: Either:
The current implementation works correctly for the user experience, but the architectural separation you described isn't reflected in the actual code flow. Would you like to adjust the implementation to match the intended architecture? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (1)
website/views/user.py (1)
1745-1745: Verify the email template variable name.The context dictionary uses
"action_url"as the key, but a past review flagged that thenew_chat.htmltemplate expects"chat_url". If this mismatch exists, email links will be broken.Run this script to check what variables the template actually expects:
#!/bin/bash # Check what variables are used in the new_chat.html email template rg -n "chat_url|action_url" --type=html
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
Knowledge base: Disabled due to Reviews -> Disable Knowledge Base setting
📒 Files selected for processing (1)
website/views/user.py(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
website/views/user.py (1)
website/models.py (2)
ChatRequest(3091-3105)Thread(3066-3071)
🔇 Additional comments (3)
website/views/user.py (3)
1719-1723: LGTM: Proper endpoint protection.The
@require_POSTdecorator correctly restricts this endpoint to POST requests only, and@login_requiredensures authentication. This is a good security practice for state-changing operations.
1728-1729: LGTM: Self-messaging validation.Correctly prevents users from creating threads with themselves, returning an appropriate 400 error.
1770-1782: LGTM: Thread creation logic.The unlocked recipient flow correctly finds or creates a thread between the two participants and returns the appropriate JSON response.
d9941a7 to
aed6f13
Compare
|
💬 Reminder: Unresolved Conversations Hi @e-esakman! This pull request has 1 unresolved conversation that need to be addressed. Please review and resolve the pending discussions so we can move forward with merging this PR. Thank you! 🙏 |
Description
This PR updates the messaging system to allow users to send chat requests to others who have not yet unlocked their chat feature, improving the UX for making interactions more dynamic.
Issue
Previously, when searching for a user who hadn’t unlocked their chats, the “Message” button was unavailable. This led to a poor user experience, as there was no way to initiate communication.
Changes
"Request Chat” button has been added which sends an email notification to the recipient, allowing them to unlock their chats and respond if they wish.
Key changes
models.py— add ChatRequest model and slug field for User.core.py— chat request creation logic.user.py— update start_thread logic to handle new request flow.search.html— change message box UI and button logic for messaging actions.Testing
Additionally , it fixes #3860 ensuring email initiation works both for locked and unlocked chat cases when a new user initiates a message.
Untitled.design.1.mp4
Summary by CodeRabbit
New Features
Behavior Changes
Tests
✏️ Tip: You can customize this high-level summary in your review settings.