-
-Welcome to ClefinCode, where innovation meets digital transformation. We’re proud to introduce ClefinCode Chat, a groundbreaking solution designed to revolutionize the way businesses communicate. Our expertise in web and mobile application development has led us to create a platform that enhances, secures, and streamlines communication across your organization, ensuring that your business stays ahead in today’s digital world.
-
-Comprehensive Communication Solution: ClefinCode Chat offers a full suite of multimedia messaging capabilities, allowing your team to share pictures, videos, files, and voice clips effortlessly. With an intuitive interface, our chat application facilitates easy adoption, enabling direct messaging or group conversations without the complexity.
-Advanced Features for Business Efficiency: Our application supports dynamic participation in conversations, topic-integrated discussions, and guest messaging via a website support portal, ensuring that your communication is both efficient and comprehensive. Manage privacy and collaboration within your organization with ease, fostering a secure and productive environment.
-
-Access Anywhere, Anytime: ClefinCode Chat is a free mobile app available for download from Google Play. This ensures that you and your team can stay connected, whether on the go or at the office.
+
-Open Source and Customizable: Behind ClefinCode Chat is the powerful [ERPNext system](https://erpnext.com), supported by the open-source [Frappe application](https://frappeframework.com). You can download the backend code from GitHub and install it on your own server. This flexibility allows you to customize your ERPNext instance to suit your specific business needs, seamlessly integrating with both our web and mobile applications.
+ClefinCode Chat is a business communication app built for ERPNext/Frappe. It brings internal team chat, customer support conversations, multimedia sharing, WhatsApp messaging, Telegram, Instagram, Messenger, topics, templates, notifications, and document-linked discussions into one connected workspace.
-Dedicated Support: Our support section within the app is designed to assist you whenever you need information, help with an issue, or have questions about our ERPNext services and mobile application development. We are here to ensure that your experience with ClefinCode Chat and ERPNext is nothing short of exceptional.
+The app supports direct messages, group conversations, document-linked topics, guest/support messaging, mobile access, and web chat controls designed for productivity inside ERPNext.
-
+
- Show more screenshots
+ Show more web screenshots
-
-
+
-
-
+
-
-
+
-
-
+
-
-
+
+## Documentation
+
+Full product documentation is available here:
+
+- [ClefinCode Chat Documentation](https://website.clefincode.com/clefincode_chat_docs)
+- [Web UI Features](https://website.clefincode.com/clefincode_chat_docs#web-ui-features)
+- [Twilio WhatsApp Integration](https://website.clefincode.com/clefincode_chat_docs#twilio-whatsapp-integration)
+- [Twilio Template](https://website.clefincode.com/clefincode_chat_docs#twilio-template)
+- [Meta WhatsApp Cloud API Setup](https://website.clefincode.com/clefincode_chat_docs#meta-whatsapp-cloud-api-setup)
+- [Edit and Add Contacts](https://website.clefincode.com/clefincode_chat_docs#edit-and-add-contacts)
+
## Features
-## 🌐 Comprehensive Communication Solution
+### 🌐 Comprehensive Communication Solution
+
+- 💬 **Direct & Group Messaging**: Start one-on-one conversations or group chats with selected contacts.
+- 🖼 **Multimedia Messaging**: Share images, videos, documents, links, and files directly in chat.
+- 🎙 **Voice Clips**: Record and send short voice messages from the composer.
+- 🔎 **Conversation Search**: Search chats and locate important conversations quickly.
+- 🧵 **Multiple Web Conversations**: Keep multiple chat windows open in ERPNext web.
+- 🌙 **Dark Theme**: Use a comfortable dark interface for extended work sessions.
+
+### 🚀 Business Collaboration Inside ERPNext
+
+- 🔗 **DocType / Topic Linking**: Link conversations to ERPNext records such as Task, Issue, Project, Invoice, Item, or other business documents.
+- 👥 **Contributors**: Add or remove contributors from group conversations.
+- @️⃣ **Mentions**: Mention users to direct attention in team conversations.
+- 🧩 **Topic Conversations**: Organize related messages under topics and open dedicated topic conversations.
+- 🔁 **ReLink Messages**: Link one or more existing messages to an existing or new topic.
+- 🧾 **Message Info**: View message metadata, sender, linked topic, sent time, message type, and message ID.
+
+### 📲 Multi-Platform Messaging
+
+ClefinCode Chat supports customer and business messaging across multiple channels:
-- 💬 **Direct & Group Messaging**: Smooth engagement in one-on-one or group chats.
-- 🖼 **Multimedia Messaging**: Share pictures, videos, files, and voice clips effortlessly.
-- 📱 **Intuitive Interface**: An easy-to-use application for quick adoption.
+- WhatsApp
+- Telegram
+- Instagram
+- Facebook Messenger
-## 🚀 Advanced Features for Business Efficiency
+Conversations can show platform badges so users can identify the channel directly from the chat interface.
-- 🔄 **User / Doctype Mentions**: Flexibly join and contribute to conversations and topic-integrated discussions.
-- 🌟 **Guest Messaging**: Enhance customer service with a website support portal.
-- 📲 **WhatsApp Business API Integration**: Manage WhatsApp conversations directly within your ERP, centralizing and organizing interactions for invoices, projects, and day-to-day communications.
+### 🌍 Access Anywhere, Anytime
-## 🌍 Access Anywhere, Anytime
+- 📲 **Mobile App Availability**: Free mobile app on [Google Play](https://play.google.com/store/apps/details?id=com.clefincode.chat&hl=en&gl=US) and [App Store](https://apps.apple.com/ae/app/clefincode-chat/id6478499855).
+- 💻 **Web Access**: Runs inside your ERPNext/Frappe web environment.
+- 🆘 **Support Conversation**: Contact ClefinCode Support from inside the app when enabled.
-- 📲 **Mobile App Availability**: Free to download from [Google Play](https://play.google.com/store/apps/details?id=com.clefincode.chat&hl=en&gl=US) and [App Store](https://apps.apple.com/ae/app/clefincode-chat/id6478499855), keeping you connected whether on the go or at the office.
-
-
- Show mobile screenshots
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ Show mobile screenshots
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-## 💻 Open Source and Customizable
+### 💻 Open Source and Customizable
-- 🛠 **Powered by ERPNext & Frappe**: Utilize the flexibility of open-source to customize your experience.
-- 🔄 **Seamless Integration**: Seamlessly integrate with our web and mobile applications to suit specific business needs.
+ClefinCode Chat is powered by [ERPNext](https://erpnext.com) and the [Frappe Framework](https://frappeframework.com). You can install the backend app on your own server and customize it to match your organization’s workflow.
-## 🤝 Dedicated Support
+### 🤝 Dedicated Support
-- 🆘 **In-App Support Section**: Get instant assistance, information, and answers to your ERPNext and mobile app development queries.
-- 🌟 **Exceptional Experience**: We're here to ensure your experience with ClefinCode Chat is nothing short of exceptional.
+Use the in-app support section to request help, ask questions, or discuss ERPNext and mobile app development support with the ClefinCode team.
-## Built with
+## Built With
-ClefinCode Chat is built using the [Frappe Framework](https://frappeframework.com) - an open-source full stack development framework.
+ClefinCode Chat is built using the [Frappe Framework](https://frappeframework.com), an open-source full-stack development framework.
-These are some of the tools it's built on:
+Core technologies include:
- [Python](https://www.python.org)
- [Redis](https://redis.io/)
- [MariaDB](https://mariadb.org/)
- [Socket.io](https://socket.io/)
-
-The mobile app is built using [Flutter](https://flutter.dev/)
-
+- [Flutter](https://flutter.dev/) for the mobile app
## Installation
-Since ClefinCode Chat is a Frappe app, it can be installed via [frappe-bench](https://frappeframework.com/docs/v14/user/en/bench) on your local machine or on your production site.
-
-Once you have setup your bench and your site, you can install the app via the following commands:
+Since ClefinCode Chat is a Frappe app, it can be installed via [frappe-bench](https://frappeframework.com/docs/v14/user/en/bench) on a local machine or production site.
```bash
bench get-app https://github.com/clefincode/clefincode_chat.git
@@ -131,229 +142,401 @@ bench --site yoursite.name migrate
bench build
```
-**Note:** If migrating from version < 1.3.0 to > 1.3.0, run the following command before migrate:
+**Note:** If migrating from version `< 1.3.0` to `> 1.3.0`, run the following command before migrate:
```bash
bench setup requirements
```
-## Getting Started with WhatsApp in ERPNext
-You'll first need to set up developer assets and obtain credentials from the Meta Developer Portal. Follow this guide to get started:
-[Meta Developer Portal Guide](https://developers.facebook.com/docs/whatsapp/cloud-api/get-started#set-up-developer-assets)
+## Quickstart
-### 1. Enter WhatsApp Credentials
+### Log in with your Frappe account
-
+Use your email and password, then confirm the correct server URL. You can switch servers when needed.
-### 2. Set Up the WhatsApp Profile
+- Enter your email and password.
+- Confirm the server domain, for example `erp.clefincode.com`.
+- Use **Change server** when you need to switch environments.
-
+Web access depends on your ERPNext permissions and enabled chat configuration.
-**Important Tips:**
+### Start a direct chat
-- When entering the WhatsApp number, do not include `00` or `+`. Start directly with the country code and the number, e.g., `971xxxxxxxxx`.
-
-- There are two types of WhatsApp profiles you can create:
-
- 1. **Personal** 👤: Opens a direct communication channel between the sender and the receiver.
- 2. **Support** 👥: Opens a group channel between the sender and receiver, allowing the admin to add or remove other members from the channel.
+- Open the chat panel.
+- Click the `+` button or new chat action.
+- Search and select a contact.
+- Send your first message to start the conversation.
-- After saving the WhatsApp profile, a WhatsApp template will be automatically created.
+
-
-
+### Start a group chat
-🎉Now, you can begin sending and receiving WhatsApp messages directly using our chat app within your ERP system🎉
+- Open the chat panel.
+- Start a new conversation.
+- Select multiple contacts.
+- Confirm to create the group.
+- Use group tools to manage the subject, topic, and contributors.
-
-Twilio WhatsApp Integration
+
-Connect WhatsApp through Twilio to manage inbound and outbound messages directly inside ERPNext.
+### Manage contributors
-1. Enter Twilio Credentials
+- Open group details.
+- Add contributors as needed.
+- Remove contributors when the scope changes.
+- Use **Exit group** when you no longer need access.
-Add your Twilio credentials:
+
-Twilio Account SID
+### Link chats to DocTypes and topics
-Auth Token
+Attach a chat topic to a document so the conversation stays aligned with a specific ERPNext record.
-
-2. Create a Twilio WhatsApp Profile
+- Open the chat and access topic controls.
+- Select the related DocType, such as Task, Issue, Project, Sales Invoice, or Item.
+- Save the topic so linked messages remain organized.
-Select the WhatsApp-enabled number from Twilio
+
-Select Provider: Twilio
+## Messaging & Sharing
-Choose a Type (Personal or Support) and save
+### Share media and documents
-Important tip: A template will be created automatically and its preview will be visible.
+Users can share PDFs, images, camera captures, videos, links, and other files directly in the chat thread.
-
-3. Test WhatsApp Messaging
-Test Sent Messages
+On web, users can upload files or drag and drop them into the chat window.
-Text
+### Voice clips
-Image
+Voice messages are supported from the chat composer alongside other sharing options.
-Voice note
+
-
-Test Received Messages
+## Multi-Platform Messaging Setup
-Text
+### WhatsApp setup through Meta Cloud API
-Image
+Use this option to connect WhatsApp Business Platform / Cloud API directly without Twilio.
-Voice note
+#### Requirements
-Location
+- Personal Facebook account to manage Meta Business.
+- Meta Business Portfolio for the company.
+- Work email, preferably on the company domain.
+- Official company documents for Business Verification.
+- Phone number not currently registered on WhatsApp and able to receive SMS or voice OTP.
+- Public HTTPS webhook endpoint for incoming messages and delivery statuses.
-Contact
+#### Setup steps
-
-Twilio Templates
+1. Create a Meta Business Portfolio.
+2. Complete Business Verification and enable security requirements.
+3. Create a Meta Developer App and add WhatsApp.
+4. Add and verify the WhatsApp phone number.
+5. Save the Phone Number ID and WhatsApp Business Account ID.
+6. Create a System User token with these permissions:
+ - `whatsapp_business_messaging`
+ - `whatsapp_business_management`
+7. Configure the webhook callback URL and subscribe to `messages`.
+
+Webhook endpoint example:
+
+```text
+https://YOUR_DOMAIN.com/api/method/clefincode_chat.webhook.handle
+```
-Create message templates, submit them for approval, then send them from chat using /.
+**Security:** Never expose Access Tokens in frontend code, screenshots, or GitHub. Store them in secure settings or environment variables.
-Create Twilio Text Template
+### WhatsApp setup inside ClefinCode Chat
-Enter Friendly Name
+#### 1. Enter WhatsApp credentials
-Select Template Type: twilio/text
+- Access Token
+- Webhook Verify Token
-Select Category
+
-Select Language
+#### 2. Create a WhatsApp profile
-Write message in Body
+- Enter WhatsApp Number.
+- Enter Phone Number ID.
+- Enter WhatsApp Business Account ID.
+- Select Type: **Personal** or **Support**.
-Save, submit, and wait for approval
+Important tip: When entering the WhatsApp number, do not include `00` or `+`. Start directly with the country code, for example `971xxxxxxxxx`.
-
+
+
+
-Important tips (Meta/WhatsApp template name rules)
+### Instagram setup
-Allowed characters: lowercase alphanumeric only (a-z, 0-9)
+Configure Instagram credentials and create a profile to route messages to the correct system user.
-Allowed separator: underscore _ only
+- Enter Instagram credentials.
+- Create an Instagram profile.
+- Enter Instagram Profile ID and Instagram App ID.
+- Select Type: Personal or Business.
-No spaces
+Important tip: Enter the username without the `@` symbol, for example `example_username`.
-No special characters (! @ # $ % - . etc.)
+Documentation: [Instagram setup](https://website.clefincode.com/clefincode_chat_docs#instagram-setup-web)
-Use meaningful names (example: order_delivery)
+### Telegram setup
-Name should be unique for your account if template content is unique
+Configure the Telegram bot token, confirm that the site webhook is set successfully, then create a Telegram profile and assign it to the correct system user.
-Create Twilio Media Template
+- Open BotFather in Telegram.
+- Create a new bot or select an existing one.
+- Copy the Bot Token.
+- Open the Telegram integration Bot DocType.
+- Enter a clear name.
+- Paste the Bot Token and save.
+- Make sure **Site Webhook Successfully Set** appears.
+- Create a ClefinCode Telegram Profile and assign users.
-- Enter Friendly Name
+Documentation: [Telegram setup](https://website.clefincode.com/clefincode_chat_docs#telegram-setup-web)
-- Select Template Type: twilio/media
+### Facebook Messenger setup
-- Select Category
+Configure Messenger integration and profiles to enable inbound and outbound messaging through Facebook Page IDs.
-- Select Language
+Important tip: Ensure the Messenger Profile ID matches the correct Facebook Page ID and confirm required app permissions for messaging.
-- Write message in Body
+Documentation: [Facebook Messenger setup](https://website.clefincode.com/clefincode_chat_docs#facebook-messenger-setup-web)
-- Add media link in Media URL
+## Twilio Meta Connection Guide
-- Save, submit, and wait for approval
+Use this setup when WhatsApp will be connected through Twilio instead of direct Meta Cloud API calls. Twilio acts as the provider, while Meta Business Portfolio and WABA own the WhatsApp business setup.
-
-Create Quick Reply Template
+### Requirements before Twilio onboarding
-- Enter Friendly Name
+- Upgraded Twilio account with billing enabled.
+- Meta Business Portfolio owned by the company.
+- Admin access to the Meta Business Portfolio.
+- Twilio number or external number not already registered on WhatsApp.
+- Access to receive OTP by SMS or voice call.
+- Clear WhatsApp display name aligned with the business or brand.
-- Select Template Type: quick-replay
+### Twilio onboarding steps
-- Write message in Body
+1. Buy or select a Twilio phone number that can receive the WhatsApp verification code.
+2. Go to Twilio Console → Messaging → Senders → WhatsApp Senders.
+3. Start WhatsApp Sender registration.
+4. Continue with Facebook.
+5. Authorize Twilio and select the correct Meta Business Portfolio.
+6. Create or select the WhatsApp Business Account (WABA).
+7. Enter WhatsApp Business Profile details and display name.
+8. Add and verify the phone number by SMS or voice OTP.
+9. Refresh WhatsApp Senders in Twilio until the sender is ready.
-- Add button and value in actions table
+Documentation: [Twilio Meta Connection Guide](https://website.clefincode.com/clefincode_chat_docs#twilio-meta-connection-guide)
-- Save, submit, and wait for approval
+## Twilio WhatsApp Integration
-
-Create List Picker Template
+Connect WhatsApp through Twilio to manage inbound and outbound messages directly inside ERPNext.
-Enter Friendly Name
+### 1. Enter Twilio credentials
-Select Template Type: list-picker
+Add your Twilio credentials:
-Write message in Body
+- Twilio Account SID
+- Auth Token
-Add list items in Items table
+
-Save, submit, and wait for approval
+### 2. Create a Twilio WhatsApp profile
-
-Use Variables
+- Select the WhatsApp-enabled number from Twilio.
+- Select Provider: **Twilio**.
+- Choose Type: **Personal** or **Support**.
+- Save the profile.
+
+Important tip: A template will be created automatically and its preview will be visible.
-Twilio uses variables in content templates to personalize messages. Variables follow the {{...}} syntax and are populated with dynamic data when the message is sent.
+
-You can map variables to fields from the associated DocType by filling the variable table.
+### 3. Configure Twilio webhook
-
+Configure Twilio webhooks to receive incoming WhatsApp messages and track outbound message status updates.
+
+Webhook URL:
+
+```text
+https://Base_URL/api/method/whatsapp_twillio
+```
+
+
-Important tips
+### 4. Test WhatsApp messaging
-Variables must be sequential: {{1}}, {{2}}, {{3}}
+Test sent messages:
-Variables should not be adjacent
+- Text
+- Image
+- Voice note
-Variables should not start or end the message
+
-Must have enough text: (2x + 1) non-variable words per x variables
+Test received messages:
-Avoid too many variables in short messages
+- Text
+- Image
+- Voice note
+- Location
+- Contact
+
+
-Add Variable to Media Template
+## Twilio Templates
-Set base URL of your site
+Create message templates, submit them for approval, then send them from chat using `/`.
+
+### Create Twilio text template
+
+- Enter Friendly Name.
+- Select Template Type: `twilio/text`.
+- Select Category.
+- Select Language.
+- Write the message in Body.
+- Save, submit, and wait for approval.
+
+
-Select field with attached file
+### Meta / WhatsApp template name rules
-Add default value
+- Allowed characters: lowercase alphanumeric only (`a-z`, `0-9`).
+- Allowed separator: underscore `_` only.
+- No spaces.
+- No special characters such as `! @ # $ % - .`.
+- Use meaningful names, for example `order_delivery`.
+- Keep names unique when template content is unique.
+
+### Create Twilio media template
+
+- Enter Friendly Name.
+- Select Template Type: `twilio/media`.
+- Select Category.
+- Select Language.
+- Write the message in Body.
+- Add media link in Media URL.
+- Save, submit, and wait for approval.
+
+
+
+### Create quick reply template
+
+- Enter Friendly Name.
+- Select Template Type: `quick-replay`.
+- Write the message in Body.
+- Add button and value in the actions table.
+- Save, submit, and wait for approval.
+
+
+
+### Create list picker template
+
+- Enter Friendly Name.
+- Select Template Type: `list-picker`.
+- Write the message in Body.
+- Add list items in the Items table.
+- Save, submit, and wait for approval.
+
+
+
+### Use variables
+
+Twilio variables personalize content templates. Variables use `{{...}}` syntax and are populated with dynamic data when the message is sent.
+
+Variables can be mapped to fields from the associated DocType by filling the variables table.
+
+
+
+Important tips:
+
+- Variables must be sequential: `{{1}}`, `{{2}}`, `{{3}}`.
+- Variables should not be adjacent.
+- Variables should not start or end the message.
+- Must have enough text: `(2x + 1)` non-variable words per `x` variables.
+- Avoid too many variables in short messages.
+
+### Add variable to media template
+
+- Set the base URL of your site.
+- Select the field with the attached file.
+- Add a default value.
-Attach DocType Print to Template
+
+### Attach DocType print to template
To send the connected DocType print:
-- Use a Media Template and add a variable (as above)
+- Use a Media Template and add a variable.
+- No need to add a DocType field, but a default value is required.
+- Enable **Attach Document Print**.
+- Select Print Format.
+- Select Language Format: English.
+- Select Letter Head.
-- No need to add a DocType field, but default value is required
+
-- Enable Attach Document Print
+### Send template from chat
-- Select Print Format
+- Open Chat.
+- Type `/` and wait for approved templates.
+- Select a template.
+- If linked to a DocType, choose the required document.
-- Select Language Format: English
+
-- Select Letter Head
+## ClefinCode Notification
-
+ClefinCode Notification sends WhatsApp notifications automatically based on DocType events.
-Send Template from Chat
+### Create ClefinCode Notification
-- Open Chat
+- Create a WhatsApp notification and link it to the required DocType.
+- Add a Custom Field to the DocType of type Link connected to Chat Profile.
+- Select the trigger event, such as **On Save** or **On Submit**.
+- Choose the message type: **Template** or **Normal Message**.
+- If Normal Message is selected, variables can be added dynamically from DocType fields.
+- If Template is selected, only templates linked to the selected DocType will appear.
+- Preview displays automatically when a template is selected.
+- If the selected template supports document printing, **Attach Print** is enabled automatically.
+- Configure the recipient using the selected Chat Profile.
+- Optionally attach the document as a PDF by selecting the required Print Format.
+- Save and enable the notification to activate automatic WhatsApp sending.
-- Type / and wait for approved templates
+
-- Select a template
+## ClefinCode Chat Template
-- If linked to a DocType, choose the required document
+ClefinCode Chat Template is used to create and manage reusable WhatsApp templates. Templates can include variables, optional document prints, and links to specific DocTypes.
-
+### Create template
+
+- Open ClefinCode Chat Template.
+- Enter the Template Name.
+- Select the Reference DocType, if needed.
+- Write the message.
+- If a DocType is selected, add variables similar to Twilio templates.
+- Save the template.
+
+### Test sending template
+
+- Open a chat conversation.
+- Type `/` in the chat input.
+- Select the created template.
+- If linked to a DocType, choose the required document.
+
+Documentation: [ClefinCode Chat Template](https://website.clefincode.com/clefincode_chat_docs#clefincode-chat-template)
## Manage Contacts
Add and manage contacts directly from the chat interface, including multiple identifiers per contact.
-### Manage Contacts (Admin)
+### Manage Contacts
- Click the **Chat** icon.
- Click the **+** button.
@@ -366,118 +549,199 @@ Add and manage contacts directly from the chat interface, including multiple ide
### ClefinCode Chat Profile
-A **Chat Profile** centralizes all identifiers for a person/company (WhatsApp, Telegram, Instagram, Messenger, etc.) and maps them to a system user.
+A Chat Profile centralizes all identifiers for a person or company and maps them to a system user.
-- Create a **Chat Profile** and set its name.
-- Assign a **System User** (email).
-- Add one or more contact identifiers (WhatsApp / Telegram / Instagram / Messenger).
+- Create a Chat Profile and define its name.
+- Assign a System User.
+- Add one or more contact identifiers.
+- Supported identifiers include WhatsApp, Telegram, Instagram, and Messenger.
- The profile keeps all identifiers organized in one place.
+## ClefinCode WhatsApp Profile
+
+Personal messages are delivered directly to the selected user.
+
+For Support Profiles:
+
+- **Receive By User** automatically adds the selected user to the conversation.
+- **Receiver Role** distributes messages randomly among users with that role.
+- To always include a specific user in all support chats, use **User – Receive By User**.
+
+Documentation: [ClefinCode WhatsApp Profile](https://website.clefincode.com/clefincode_chat_docs#clefincode-whatsapp-profile)
## 🖥 Web UI Features
-Powerful and intuitive message controls designed to enhance productivity and keep conversations structured inside ERPNext.
+Powerful and intuitive message controls designed to enhance productivity, organize conversations by topics, and keep discussions structured inside ERPNext.
+
+### ⚡ Message Actions Menu
+
+Each message has a small arrow beside it. Click this arrow to open available message actions.
+
+Available actions include:
+
+- **Edit** — edit the message content.
+- **Reply** — reply to a specific message.
+- **Copy** — copy message text.
+- **React** — add an emoji reaction.
+- **Forward** — forward one or more messages.
+- **ReLink** — link one or more messages to a topic.
+- **Delete** — delete the message.
+- **Message Info** — view message details.
+
+
### ↩️ Reply to Messages
-To reply to a specific message:
-1. Click directly on the message bubble.
+
+Reply to a specific message and keep the context linked.
+
+1. Click the small arrow beside the message.
2. Select **Reply**.
3. Type your response and send.
-Your reply will remain linked to the original message to preserve context and make conversations easier to follow.
-
-
+
-**GIF Link:** screenshots/web/reply_message.gif
+### 🔁 Forward Messages
----
+Forward supports multi-select, allowing users to forward one or more messages at the same time.
-### 🔁 Forward Messages
-To forward a message:
-1. Click on the message bubble.
+1. Click the small arrow beside a message.
2. Select **Forward**.
-3. Choose the contact or channel you want to forward the message to.
-4. You can select **multiple recipients at the same time**.
-5. Confirm to send.
-
-Forwarded messages are clearly labeled for transparency.
+3. The chat enters selection mode and checkboxes appear beside messages.
+4. Select one or more messages.
+5. Choose the contact, group, or channel.
+6. Confirm to forward the selected messages.
-
+
-**GIF Link:** screenshots/web/forward_message.gif
+### ✏️ Edit Sent Messages
----
+Edit a sent message using a rich text editor with formatting tools.
-### ✏️ Edit Sent Messages
-To edit a message:
-1. Click on the message bubble.
+1. Click the small arrow beside the message.
2. Select **Edit**.
-3. Update the message content.
-4. Save the changes.
+3. The edit dialog opens with a rich text editor.
+4. Update the message content.
+5. Click **Save** to apply changes, or **Cancel** to close without saving.
-⚠️ Editing is only allowed within **7 minutes** of sending the message.
-This duration can be modified from the **Chat Settings**.
+The editor supports formatting such as bold, italic, underline, lists, links, images, alignment, and tables.
-
+**Important:** Editing is only allowed within **7 minutes** of sending the message. This can be changed from **Chat Settings**.
-**GIF Link:** screenshots/web/edit_message.gif
-
----
+
### 🗑 Delete Messages
-To delete a message:
-1. Click on the message bubble.
+
+Delete a message within the configured time limit.
+
+1. Click the small arrow beside the message.
2. Select **Delete**.
3. Confirm the deletion.
-⚠️ **Important:**
-Messages cannot be edited or deleted after **7 minutes** from the time they were sent.
-The time limit can be customized from the **Chat Settings**.
+**Important:** Messages cannot be edited or deleted after **7 minutes** from sending, unless the time limit is changed from **Chat Settings**.
-
+
-**GIF Link:** screenshots/web/delete_message.gif
+### 😀 Message Reactions
----
+React with emojis to reduce unnecessary replies.
-### 😀 Message Reactions
-To react to a message:
-1. Press and hold (long press) on the message bubble.
-2. Select the desired emoji reaction.
+1. Click the small arrow beside the message.
+2. Select **React**.
+3. Choose the desired emoji reaction.
-You can:
-- Remove a reaction by clicking the same emoji again.
-- Change your reaction by selecting a different emoji.
+You can remove a reaction by clicking the same emoji again, or change your reaction by selecting a different emoji.
-Reactions help reduce unnecessary replies and improve collaboration speed.
+
-
+### 🔗 ReLink Messages to Topic
-**GIF Link:** screenshots/web/reactions.gif
+ReLink allows users to connect one or more messages to an existing topic or create a new topic.
----
+1. Click the small arrow beside a message.
+2. Select **ReLink**.
+3. The chat enters selection mode and checkboxes appear beside messages.
+4. Select one or more messages.
+5. Click **ReLink Topic**.
+6. Select an existing topic, or click **Add New Topic** to create a new one.
-### 🔎 In-Chat Search
-1. Open the desired chat (contact or channel).
-2. Click on the top area of the conversation (chat header).
-3. The chat details interface will open.
-4. Click on the **Search icon**.
-5. Enter the keyword you want to find.
-6. Navigate through the results to locate the exact message.
+ReLink helps organize related messages under a specific topic, making it easier to follow discussions connected to a document, issue, invoice, item, or work context.
+
+
+
+### 🧵 Topic Conversations
+
+After messages are linked to a topic, the topic appears inside the main conversation as a colored topic bar.
+
+- Each topic has its own color to make it easy to identify.
+- Click the topic bar or arrow to open a separate conversation for that topic.
+- Topic conversations show only messages linked to that topic.
+- The topic chat header displays the topic name.
+- From the topic header, users can add or remove linked DocTypes.
+- Users can update the topic Status, Color, and Subject.
+
+
+
+
+### ➕ Chat Plus Menu
-This allows you to quickly find important details, decisions, or shared information.
+The plus button beside the chat input gives quick access to attachments and topic actions.
+
+Available actions:
+
+- **Attach File** — upload and send a file in the conversation.
+- **Select Topic** — choose an existing topic and open or link to it.
+- **Add New Topic** — create a new topic from the chat.
+
+
+
+### 🏷 Select Topic
+
+Select Topic lets users search and choose from existing topics directly inside the chat.
+
+1. Click the `+` button beside the chat input.
+2. Select **Select Topic**.
+3. Use the search field to find a topic.
+4. Click the arrow beside the topic to open or select it.
+
+Each topic appears with its own colored indicator. A number may appear beside the topic name to show related message count or topic activity.
+
+
+
+### ℹ️ Message Info
+
+Message Info displays detailed information about a selected message.
+
+1. Click the small arrow beside the message.
+2. Select **Message Info**.
+3. Review message details in the popup.
+
+Information includes:
+
+- Sender
+- Linked Topic
+- Sent At
+- Message Type
+- Message ID
+
+
+
+### 🔎 In-Chat Search
-
+Find messages quickly inside the same conversation.
-**GIF Link:** screenshots/web/search.gif
+1. Open the desired chat.
+2. Click the top area of the conversation.
+3. Click the Search icon.
+4. Enter your keyword and navigate results.
+
## Reporting Bugs
-If you find any bugs, feel free to report them here on [GitHub Issues](https://github.com/clefincode/clefincode_chat/issues).
+If you find any bugs, report them through [GitHub Issues](https://github.com/clefincode/clefincode_chat/issues).
## License
-GNU General Public License (v3)
\ No newline at end of file
+GNU General Public License (v3)
diff --git a/clefincode_chat/__init__.py b/clefincode_chat/__init__.py
index a16e534..7459b19 100644
--- a/clefincode_chat/__init__.py
+++ b/clefincode_chat/__init__.py
@@ -1 +1 @@
-__version__ = '1.3.903'
\ No newline at end of file
+__version__ = '1.3.907'
\ No newline at end of file
diff --git a/clefincode_chat/api/api_1_3_3/api.py b/clefincode_chat/api/api_1_3_3/api.py
index fd4c59a..4dce0a8 100644
--- a/clefincode_chat/api/api_1_3_3/api.py
+++ b/clefincode_chat/api/api_1_3_3/api.py
@@ -48,7 +48,7 @@
from frappe import _
from frappe.desk.search import validate_and_sanitize_search_inputs
-
+from packaging import version
TOPIC_COLOR_PALETTE = [
@@ -3603,7 +3603,7 @@ def search_in_message_content(user , query):
return my_messages
# ==========================================================================================
@frappe.whitelist()
-def search_in_message_contents(channel, query, sub_channel=None):
+def search_in_message_contents(channel, query, sub_channel=None, chat_topic=None):
if not channel or not query:
return {"results": []}
@@ -3616,6 +3616,9 @@ def search_in_message_contents(channel, query, sub_channel=None):
if sub_channel:
filters["sub_channel"] = sub_channel
+ if chat_topic:
+ filters["chat_topic"] = chat_topic
+
results = frappe.get_all(
"ClefinCode Chat Message",
filters=filters,
diff --git a/clefincode_chat/api/api_1_3_4/api.py b/clefincode_chat/api/api_1_3_4/api.py
index 8f8c6d5..45eb314 100644
--- a/clefincode_chat/api/api_1_3_4/api.py
+++ b/clefincode_chat/api/api_1_3_4/api.py
@@ -46,7 +46,7 @@
from frappe.utils.password import check_password, update_password
from frappe.sessions import clear_sessions
from frappe.desk.search import validate_and_sanitize_search_inputs
-
+from packaging import version
@@ -325,7 +325,7 @@ def get_registration_tokens(user_email):
"registration_token": row.registration_token,
"platform": row.platform
})
- frappe.log_error('tokens',tokens)
+
return tokens
def get_registration_token(user_email):
@@ -468,10 +468,31 @@ def get_social_config_for_user(user):
}
# ==========================================================================================
+DEFAULT_LIMITED_ROLES = [
+ "Customer",
+ "Supplier",
+ "Student",
+ "Instructor",
+ "Sales Partner",
+ "Member",
+ "Shareholder",
+ "Guardian",
+]
def is_limited_user(user):
- roles = frappe.get_roles(user)
- limited_roles = ["Customer", "Supplier", "Student", "Instructor", "Sales Partner", "Member", "Shareholder", "Guardian"]
- return any(role in roles for role in limited_roles)
+ user_roles = frappe.get_roles(user)
+
+ settings = frappe.get_single("ClefinCode Chat Settings")
+
+ limited_roles = [
+ row.role
+ for row in settings.limited_roles
+ if row.role
+ ]
+
+ if not limited_roles:
+ limited_roles = DEFAULT_LIMITED_ROLES
+
+ return any(role in user_roles for role in limited_roles)
# ==========================================================================================
def get_user_type():
if frappe.session.user == "Guest":
@@ -666,20 +687,12 @@ def create_sub_channel(new_contributors , parent_channel , user , user_email , c
frappe.publish_realtime(event= parent_channel, message=results, user= member.user)
results2 = {'parent_channel' : parent_channel, "sub_channel" : "" , "realtime_type" : "create_sub_channel", "target_user" : user_to_remove, "chat_topic": chat_topic[0].name if chat_topic else None}
- # frappe.publish_realtime(event= "receive_message", message= results2, user= user_to_remove)
- frappe.publish_realtime(event= last_active_sub_channel, message={'parent_channel' : parent_channel, "sub_channel" : "" , "realtime_type" : "create_sub_channel"}, user= user_to_remove)
- notification_title = get_room_name(parent_channel, "Contributor")
- send_notification(user_to_remove , results2, "create_sub_channel", notification_title)
return {"results" : [{"channel" : parent_channel}]}
else:
if user_to_remove:
res = {'parent_channel' : parent_channel, "sub_channel" : "" , "realtime_type" : "create_sub_channel" , "target_user" : user_to_remove, "chat_topic": chat_topic[0].name if chat_topic else None}
disable_contributor(parent_channel_doc , user_to_remove)
frappe.db.sql(f"""UPDATE `tabClefinCode Chat Channel User` SET active = 0 WHERE parent = '{last_active_sub_channel}' AND user = '{user_to_remove}'""")
- # frappe.publish_realtime(event= "receive_message", message= res, user= user_to_remove)
- frappe.publish_realtime(event= last_active_sub_channel, message={'parent_channel' : parent_channel, "sub_channel" : "" , "realtime_type" : "create_sub_channel"}, user= user_to_remove)
- notification_title = get_room_name(parent_channel, "Contributor")
- send_notification(user_to_remove , res, "create_sub_channel", notification_title)
if isinstance(new_contributors , str):
new_contributors = json.loads(new_contributors)
@@ -768,18 +781,11 @@ def leave_contributor(parent_channel , user , creation_date = None , last_active
if member.platform == "Chat":
frappe.publish_realtime(event= parent_channel, message=results, user= member.user)
res = {'parent_channel' : parent_channel, "sub_channel" : "" , "realtime_type" : "create_sub_channel", "target_user" : user_to_remove}
- # frappe.publish_realtime(event= "receive_message", message= res, user= user_to_remove)
- frappe.publish_realtime(event= last_active_sub_channel, message={'parent_channel' : parent_channel, "sub_channel" : "" , "realtime_type" : "create_sub_channel"}, user= user_to_remove)
- send_notification(user_to_remove , res, "create_sub_channel")
-
return {"results" : [{"channel" : parent_channel}]}
else:
res = {'parent_channel' : parent_channel, "sub_channel" : "" , "realtime_type" : "create_sub_channel" , "target_user" : user_to_remove}
disable_contributor(parent_channel_doc , user_to_remove)
frappe.db.sql(f"""UPDATE `tabClefinCode Chat Channel User` SET active = 0 WHERE parent = '{last_active_sub_channel}' AND user = '{user_to_remove}'""")
- # frappe.publish_realtime(event="receive_message", message= res, user= user_to_remove)
- frappe.publish_realtime(event=last_active_sub_channel, message={'parent_channel' : parent_channel, "sub_channel" : "" , "realtime_type" : "create_sub_channel"}, user= user_to_remove)
- send_notification(user_to_remove , res, "create_sub_channel")
sub_channel_doc = frappe.get_doc({
@@ -1186,8 +1192,9 @@ def get_all_sub_channels_for_contributor(parent_channel , user_email):
#############################################################################################
######################################## Messages ###########################################
#############################################################################################
+
@frappe.whitelist()
-def send(content, user, room , email, send_date = None , is_first_message = 0,is_forwarded=0, attachment = None , sub_channel = None , is_link = None , is_media = None , is_document = None, is_voice_clip = None , file_id = None , message_type = "" , message_template_type= "", only_receive_by = None , id_message_local_from_app = None , id_channel_local_from_app = None , chat_topic = None, is_screenshot = 0,reply_to_message_name=None,forwarded_from=None,whatsapp_message_id=None):
+def send(content, user, room , email, send_date = None , is_first_message = 0,is_forwarded=0, attachment = None , sub_channel = None , is_link = None , is_media = None , is_document = None, is_voice_clip = None , file_id = None , message_type = "" , message_template_type= "", only_receive_by = None , id_message_local_from_app = None , id_channel_local_from_app = None , chat_topic = None, is_screenshot = 0,reply_to_message_name=None,forwarded_from=None,whatsapp_message_id=None,override_variables=None ):
try:
@@ -1258,7 +1265,10 @@ def send(content, user, room , email, send_date = None , is_first_message = 0,is
new_message.file_id = frappe.db.get_value("File" , {"attached_to_name": new_message.name}, "name")
new_message.save(ignore_permissions = True)
- if attachment: set_attach_message(attachment, new_message.name)
+ if file_id:
+ set_attach_message(file_id=file_id, message_name=new_message.name)
+ elif attachment:
+ set_attach_message(attachment=attachment, message_name=new_message.name)
if reply_to_message_name:
try:
@@ -1370,9 +1380,21 @@ def send(content, user, room , email, send_date = None , is_first_message = 0,is
"utc_message_date" : send_date,
"is_forwarded":is_forwarded,
- "platform": platform
+ "platform": platform ,
+ "chat_channel": room,
+ "chat_topic": chat_topic,
}
+ if chat_topic:
+ try:
+ topic_info = frappe.db.get_value("ClefinCode Chat Topic", chat_topic, ["topic_subject", "topic_color"], as_dict=True)
+ if topic_info:
+ results["chat_topic_subject"] = topic_info.topic_subject
+ results["topic_color"] = topic_info.topic_color
+ except Exception:
+ pass
+
+
if reply_to_message_name :
results.update({"reply_to_message":reply_to_message_name,
"reply_preview_type": new_message.reply_preview_type ,
@@ -1464,7 +1486,7 @@ def send(content, user, room , email, send_date = None , is_first_message = 0,is
send_notification(member.user , results, "send_message", room_name if channel_doc.type == "Group" else get_contact_full_name(email), message_template_type)
elif member.platform == "WhatsApp" and email != member.user and message_template_type not in ["Rename Group" , "Send Confirmation"] and not is_mention(content) and member.is_removed == 0:
- process_whatsapp_message(member.platform_gateway, member.user , email, channel_doc, last_responder_user, new_message, file_type, attachment, content, is_voice_clip, is_screenshot,results,is_forwarded)
+ process_whatsapp_message(member.platform_gateway, member.user , email, channel_doc, last_responder_user, new_message, file_type, attachment, content, is_voice_clip, is_screenshot,results,is_forwarded,override_variables )
if member.pending_messages >= 1:
frappe.db.set_value('ClefinCode Chat Channel User', member.name, 'pending_messages', member.pending_messages +1)
elif member.platform == "Instagram" and str(email) != str(member.user) and message_template_type not in ["Rename Group" , "Send Confirmation"] and not is_mention(content) and member.is_removed == 0:
@@ -1614,6 +1636,7 @@ def get_messages(room, user_email, room_type, chat_topic=None,
msg.chat_topic,
topic.subject AS chat_topic_subject,
topic.topic_color AS topic_color,
+ topic.topic_status AS topic_status,
msg.is_edited,
backup.original_content AS original_content
FROM `tabClefinCode Chat Message` msg
@@ -1653,6 +1676,7 @@ def get_messages(room, user_email, room_type, chat_topic=None,
msg.chat_topic,
topic.subject AS chat_topic_subject,
topic.topic_color AS topic_color,
+ topic.topic_status AS topic_status,
msg.is_edited,
backup.original_content AS original_content
FROM `tabClefinCode Chat Message` msg
@@ -2563,13 +2587,39 @@ def get_file_view_size(file_id,is_video=None):
file_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
return {"results" : [{'file_size':file_doc.file_size,'data':file_base64,'duration':duration}]}
# ==========================================================================================
-def set_attach_message(attachment, message):
- file_doc = frappe.get_doc("File", {"file_url": attachment})
- file_doc.update({
- "attached_to_doctype": "ClefinCode Chat Message",
- "attached_to_name": message
- })
- file_doc.save(ignore_permissions = True)
+def set_attach_message(file_id=None, attachment=None, message_name=None):
+ import frappe
+ from urllib.parse import urlparse, unquote
+
+ file_doc = None
+
+ if file_id:
+ file_doc = frappe.get_doc("File", file_id)
+
+ elif attachment:
+ parsed = urlparse(attachment)
+
+ file_url = parsed.path if parsed.scheme and parsed.netloc else attachment
+ file_url = unquote(file_url)
+
+ if not file_url.startswith("/"):
+ file_url = "/" + file_url
+
+ file_name = frappe.db.get_value("File", {"file_url": file_url}, "name")
+
+ if not file_name:
+ frappe.throw(f"File not found yet or wrong file_url: {file_url}")
+
+ file_doc = frappe.get_doc("File", file_name)
+
+ if not file_doc:
+ frappe.throw("No file_id or attachment provided")
+
+ file_doc.attached_to_doctype = "ClefinCode Chat Message"
+ file_doc.attached_to_name = message_name
+ file_doc.save(ignore_permissions=True)
+
+ return file_doc
# ==========================================================================================
def get_file_type(file_name):
ext = os.path.splitext(file_name)[1] # Get the file extension
@@ -2873,8 +2923,9 @@ def ensure_video_thumbnail(message_doc, max_size=(320, 320), quality=80):
#############################################################################################
@frappe.whitelist()
def get_topic_info(chat_channel):
- chat_topic = frappe.get_all("ClefinCode Chat Topic" , ["name" , "subject" , "is_private"] , {"chat_channel":chat_channel , "topic_status" : "Open"})
+ chat_topic = frappe.get_all("ClefinCode Chat Topic" , ["name" , "subject" , "is_private" , "topic_color"] , {"chat_channel":chat_channel , "topic_status" : "Open"})
if chat_topic:
+ topic_color = chat_topic[0].topic_color
reference_doctypes = frappe.db.sql(f"""
SELECT doctype_link AS doctype , docname
FROM `tabClefinCode Chat Topic Reference`
@@ -2882,9 +2933,9 @@ def get_topic_info(chat_channel):
ORDER BY idx
""" , as_dict = True)
if reference_doctypes:
- return {"results" : [{"chat_topic" : chat_topic[0].name , "reference_doctypes" : reference_doctypes, "chat_topic_subject" : chat_topic[0].subject , "chat_topic_status": "private" if chat_topic[0].is_private == 1 else "public"}]}
+ return {"results" : [{"chat_topic" : chat_topic[0].name , "reference_doctypes" : reference_doctypes, "chat_topic_subject" : chat_topic[0].subject , "topic_color": topic_color, "chat_topic_status": "private" if chat_topic[0].is_private == 1 else "public"}]}
else:
- return {"results" : [{"chat_topic" : chat_topic[0].name , "reference_doctypes" : [] , "chat_topic_subject" : chat_topic[0].subject , "chat_topic_status": "private" if chat_topic[0].is_private == 1 else "public"}]}
+ return {"results" : [{"chat_topic" : chat_topic[0].name , "reference_doctypes" : [] , "chat_topic_subject" : chat_topic[0].subject , "topic_color": topic_color, "chat_topic_status": "private" if chat_topic[0].is_private == 1 else "public"}]}
else:
return {"results" : [{"chat_topic" : None , "reference_doctypes" : [] , "chat_topic_subject" : None}]}
# ==========================================================================================
@@ -2901,13 +2952,22 @@ def get_references_doctypes(chat_topic):
return {"results" : [{"reference_doctypes" : reference_doctypes , "chat_topic_subject" : frappe.db.get_value("ClefinCode Chat Topic" , chat_topic, "subject") , "chat_topic_status" : frappe.db.get_value("ClefinCode Chat Topic" , chat_topic, "is_private")}]}
# ==========================================================================================
@frappe.whitelist()
-def create_chat_topic(mention_doctypes, chat_channel, last_active_sub_channel = None):
+def create_chat_topic(mention_doctypes, chat_channel, last_active_sub_channel = None, subject = None):
mention_doctypes = json.loads(mention_doctypes)
+
+ if not subject and not mention_doctypes:
+ frappe.throw("Subject or at least one reference is required to create a topic.")
+
+ if not subject and mention_doctypes:
+ subject = "{}:{}".format(mention_doctypes[0]["doctype"], mention_doctypes[0]["docname"])
+
chat_topic = frappe.get_doc({
"doctype" : "ClefinCode Chat Topic",
"chat_channel" : chat_channel,
"topic_status": "Open",
- "is_private" : 1
+ "is_private" : 1,
+ "topic_color": get_random_topic_color(),
+ "subject": subject,
}).insert(ignore_permissions = True)
for doc in mention_doctypes:
chat_topic.append("references", {"doctype_link": doc["doctype"] , "docname":doc["docname"], "active":1})
@@ -2917,7 +2977,10 @@ def create_chat_topic(mention_doctypes, chat_channel, last_active_sub_channel =
results = {
"realtime_type" : "set_topic",
"chat_topic": chat_topic.name,
- "mention_doctypes" : mention_doctypes
+ "mention_doctypes" : mention_doctypes,
+ "topic_color": chat_topic.topic_color
+
+
}
for member in frappe.get_doc("ClefinCode Chat Channel" , chat_channel).members:
@@ -2939,12 +3002,15 @@ def create_chat_topic(mention_doctypes, chat_channel, last_active_sub_channel =
# frappe.publish_realtime(event="receive_message", message=results, user= contributor.user)
send_notification(contributor.user , results, "set_topic")
- return {"results" : [{"chat_topic" : chat_topic.name}]}
+ return {"results" : [{"chat_topic" : chat_topic.name, "topic_color" : chat_topic.topic_color, "subject": subject, "chat_topic_subject": subject}]}
+
# ==========================================================================================
@frappe.whitelist()
def remove_chat_topic(chat_topic, chat_channel, last_active_sub_channel = None):
frappe.db.set_value("ClefinCode Chat Topic" , chat_topic , "topic_status" , "Closed")
+ clear_active_chat_topic_for_all(chat_channel, chat_topic)
+
results = {
"realtime_type" : "remove_topic"
}
@@ -2967,18 +3033,513 @@ def remove_chat_topic(chat_topic, chat_channel, last_active_sub_channel = None):
send_notification(contributor.user , results, "remove_topic")
return {"results" : [{"chat_topic_subject" : frappe.db.get_value("ClefinCode Chat Topic" , chat_topic , "subject")}]}
+
+
+def _get_chat_topic_subject_for_close(chat_topic):
+ if not chat_topic:
+ return None
+
+ topic_subject = frappe.db.get_value("ClefinCode Chat Topic", chat_topic, "subject")
+ if topic_subject:
+ return topic_subject
+
+ first_reference = frappe.get_all(
+ "ClefinCode Chat Topic Reference",
+ filters={
+ "parent": chat_topic,
+ "parenttype": "ClefinCode Chat Topic",
+ "parentfield": "references",
+ "active": 1
+ },
+ fields=["docname"],
+ order_by="idx asc",
+ limit_page_length=1
+ )
+
+ if first_reference:
+ return first_reference[0].docname
+
+ return chat_topic
+
+
+def _publish_closed_topic_realtime(
+ chat_topic,
+ chat_channel,
+ chat_topic_subject=None,
+ last_active_sub_channel=None,
+ realtime_type="remove_topic"
+):
+ results = {
+ "realtime_type": realtime_type,
+ "chat_topic": chat_topic,
+ "chat_topic_subject": chat_topic_subject,
+ "topic_status": "Closed",
+ "chat_topic_status": "Closed",
+ "clear_message_topic": 1
+ }
+
+ for member in frappe.get_doc("ClefinCode Chat Channel" , chat_channel).members:
+ if member.is_removed == 0 and member.platform == "Chat":
+ member_results = dict(results)
+ member_results["room"] = chat_channel
+ member_results["target_user"] = member.user
+ frappe.publish_realtime(event= chat_channel, message=member_results, user=member.user)
+ frappe.publish_realtime(event="receive_message", message=member_results, user= member.user)
+ send_notification(member.user , member_results, realtime_type)
+
+ for contributor in frappe.get_doc("ClefinCode Chat Channel" , chat_channel).contributors:
+ if contributor.active == 1 and contributor.platform == "Chat":
+ notification_results = None
+ for contributor_room in _get_realtime_contributor_rooms(chat_channel, contributor, last_active_sub_channel):
+ contributor_results = dict(results)
+ contributor_results["room"] = contributor_room
+ contributor_results["parent_channel"] = chat_channel
+ contributor_results["target_user"] = contributor.user
+ if notification_results is None:
+ notification_results = contributor_results
+ frappe.publish_realtime(event= contributor_room, message=contributor_results, user=contributor.user)
+ frappe.publish_realtime(event="receive_message", message=contributor_results, user= contributor.user)
+ if notification_results:
+ send_notification(contributor.user , notification_results, realtime_type)
+
+
+def _get_realtime_contributor_rooms(chat_channel, contributor=None, last_active_sub_channel=None):
+ rooms = []
+
+ def add_room(room):
+ if room and room not in rooms:
+ rooms.append(room)
+
+ add_room(last_active_sub_channel)
+ add_room(getattr(contributor, "channel", None))
+
+ open_sub_channels = frappe.get_all(
+ "ClefinCode Chat Channel",
+ filters={"parent_channel": chat_channel, "chat_status": "Open"},
+ pluck="name"
+ )
+
+ for sub_channel in open_sub_channels:
+ add_room(sub_channel)
+
+ add_room(chat_channel)
+ return rooms
+
+
+def _publish_clear_message_topic_realtime(
+ chat_channel,
+ chat_topic=None,
+ chat_topic_subject=None,
+ last_active_sub_channel=None
+):
+ results = {
+ "realtime_type": "clear_message_topic",
+ "chat_topic": chat_topic,
+ "chat_topic_subject": chat_topic_subject,
+ "clear_message_topic": 1
+ }
+
+ channel = frappe.get_doc("ClefinCode Chat Channel", chat_channel)
+
+ for member in channel.members:
+ if member.is_removed == 0 and member.platform == "Chat":
+ member_results = dict(results)
+ member_results["room"] = chat_channel
+ member_results["target_user"] = member.user
+ frappe.publish_realtime(event=chat_channel, message=member_results, user=member.user)
+ frappe.publish_realtime(event="receive_message", message=member_results, user=member.user)
+
+ for contributor in channel.contributors:
+ if contributor.active == 1 and contributor.platform == "Chat":
+ for contributor_room in _get_realtime_contributor_rooms(chat_channel, contributor, last_active_sub_channel):
+ contributor_results = dict(results)
+ contributor_results["room"] = contributor_room
+ contributor_results["parent_channel"] = chat_channel
+ contributor_results["target_user"] = contributor.user
+ frappe.publish_realtime(event=contributor_room, message=contributor_results, user=contributor.user)
+ frappe.publish_realtime(event="receive_message", message=contributor_results, user=contributor.user)
+
+
+@frappe.whitelist()
+def clear_message_topic(chat_channel, chat_topic=None, last_active_sub_channel=None):
+ if not chat_channel and chat_topic:
+ chat_channel = frappe.db.get_value("ClefinCode Chat Topic", chat_topic, "chat_channel")
+
+ if not chat_channel:
+ frappe.throw(_("chat_channel is required"))
+
+ chat_topic_subject = _get_chat_topic_subject_for_close(chat_topic) if chat_topic else None
+ _publish_clear_message_topic_realtime(
+ chat_channel=chat_channel,
+ chat_topic=chat_topic,
+ chat_topic_subject=chat_topic_subject,
+ last_active_sub_channel=last_active_sub_channel
+ )
+
+ return {
+ "results": [{
+ "status": "Cleared",
+ "chat_channel": chat_channel,
+ "chat_topic": chat_topic,
+ "chat_topic_subject": chat_topic_subject,
+ "clear_message_topic": 1
+ }]
+ }
+
+
+@frappe.whitelist()
+def close_chat_topic(chat_channel, chat_topic=None, last_active_sub_channel=None, user_email=None, user_name=None):
+ if not chat_channel and chat_topic:
+ chat_channel = frappe.db.get_value("ClefinCode Chat Topic", chat_topic, "chat_channel")
+
+ if not chat_channel:
+ frappe.throw(_("chat_channel is required"))
+
+ if not chat_topic:
+ open_topic = frappe.get_all(
+ "ClefinCode Chat Topic",
+ filters={"chat_channel": chat_channel, "topic_status": "Open"},
+ fields=["name"],
+ order_by="creation desc",
+ limit_page_length=1
+ )
+ chat_topic = open_topic[0].name if open_topic else None
+
+ if not chat_topic:
+ frappe.throw(_("No open topic found for this channel."))
+
+ topic = frappe.get_doc("ClefinCode Chat Topic", chat_topic)
+ if topic.chat_channel and topic.chat_channel != chat_channel:
+ frappe.throw(_("Topic does not belong to this channel."))
+
+ chat_topic_subject = _get_chat_topic_subject_for_close(chat_topic)
+ should_send_close_message = topic.topic_status != "Closed"
+
+ if should_send_close_message:
+ topic.topic_status = "Closed"
+ topic.save(ignore_permissions=True)
+ frappe.db.commit()
+
+ clear_active_chat_topic_for_all(chat_channel, chat_topic)
+
+ _publish_closed_topic_realtime(
+ chat_topic=chat_topic,
+ chat_channel=chat_channel,
+ chat_topic_subject=chat_topic_subject,
+ last_active_sub_channel=last_active_sub_channel,
+ realtime_type="close_topic"
+ )
+
+ if should_send_close_message:
+ if not user_email:
+ session_user = frappe.session.user
+ user_email = frappe.db.get_value("User", session_user, "email") or session_user
+
+ sender_name = user_name or get_profile_id(user_email) or user_email
+ safe_user_email = frappe.utils.escape_html(user_email or "")
+ safe_topic_subject = frappe.utils.escape_html(chat_topic_subject or chat_topic)
+ close_content = f"""
+
+ ${__("A limited user can communicate with internal users or support team members, but cannot see or start conversations with other limited users.")}
+
+
+ ${__("This restriction helps protect external users' privacy and prevents them from discovering or contacting each other inside the system.")}
+