mirror of
https://github.com/chatwoot/chatwoot.git
synced 2026-06-13 21:01:16 +08:00
Voice calling is now a capability on the existing TwilioSms rather than a separate Voice model. A single Twilio phone number handles both SMS and voice calls through one inbox. Fixes https://linear.app/chatwoot/issue/CW-6683/add-voice-calling-as-a-capability-on-twilio-sms-channel and https://linear.app/chatwoot/issue/PLA-120/add-the-support-for-sms **What changed** - Replaced Channel::Voice with voice_enabled flag on Channel::TwilioSms - Added voice_enabled, twiml_app_sid, api_key_secret columns to channel_twilio_sms table - Dropped channel_voice table (no production data) - All voice logic lives in Enterprise layer via prepend_mod_with('Channel::TwilioSms') - Added Voice settings tab on Twilio SMS inbox settings to enable/disable voice - Validates Twilio number voice capability before provisioning - Teardown service cleans up TwiML app and credentials when voice is disabled - Frontend voice detection uses isVoiceCallEnabled() / getVoiceCallProvider() helpers — extensible to future providers - Gated by channel_voice feature flag **How to test** 1. Enable feature flag: Account.find(<id>).enable_features('channel_voice') 2. Create voice inbox: Inboxes → Voice tile → enter Twilio credentials → verify incoming/outgoing calls and SMS work 3. Enable voice on existing SMS inbox: Inboxes → select Twilio SMS inbox → Voice tab → toggle on → provide API key credentials → verify calls work 4. Disable voice: Voice tab → toggle off → verify TwiML app is deleted, credentials cleared, SMS still works 5. Re-enable voice: Toggle on again → must provide api_key_secret again → new TwiML app provisioned --------- Co-authored-by: Muhsin <[email protected]>
286 lines
6.7 KiB
JavaScript
286 lines
6.7 KiB
JavaScript
// Formatting rules for different contexts (channels and special contexts)
|
|
// marks: inline formatting (strong, em, code, link, strike)
|
|
// nodes: block structures (bulletList, orderedList, codeBlock, blockquote)
|
|
export const FORMATTING = {
|
|
// Channel formatting
|
|
'Channel::Email': {
|
|
marks: ['strong', 'em', 'code', 'link'],
|
|
nodes: ['bulletList', 'orderedList', 'codeBlock', 'blockquote', 'image'],
|
|
menu: [
|
|
'copilot',
|
|
'strong',
|
|
'em',
|
|
'code',
|
|
'link',
|
|
'bulletList',
|
|
'orderedList',
|
|
'undo',
|
|
'redo',
|
|
],
|
|
},
|
|
'Channel::WebWidget': {
|
|
marks: ['strong', 'em', 'code', 'link', 'strike'],
|
|
nodes: ['bulletList', 'orderedList', 'codeBlock', 'blockquote', 'image'],
|
|
menu: [
|
|
'copilot',
|
|
'strong',
|
|
'em',
|
|
'code',
|
|
'link',
|
|
'strike',
|
|
'bulletList',
|
|
'orderedList',
|
|
'undo',
|
|
'redo',
|
|
],
|
|
},
|
|
'Channel::Api': {
|
|
marks: ['strong', 'em'],
|
|
nodes: [],
|
|
menu: ['copilot', 'strong', 'em', 'undo', 'redo'],
|
|
},
|
|
'Channel::FacebookPage': {
|
|
marks: ['strong', 'em', 'code', 'strike'],
|
|
nodes: ['bulletList', 'orderedList', 'codeBlock'],
|
|
menu: [
|
|
'copilot',
|
|
'strong',
|
|
'em',
|
|
'code',
|
|
'strike',
|
|
'bulletList',
|
|
'orderedList',
|
|
'undo',
|
|
'redo',
|
|
],
|
|
},
|
|
'Channel::TwitterProfile': {
|
|
marks: [],
|
|
nodes: [],
|
|
menu: [],
|
|
},
|
|
'Channel::TwilioSms': {
|
|
marks: [],
|
|
nodes: [],
|
|
menu: [],
|
|
},
|
|
'Channel::Sms': {
|
|
marks: [],
|
|
nodes: [],
|
|
menu: [],
|
|
},
|
|
'Channel::Whatsapp': {
|
|
marks: ['strong', 'em', 'code', 'strike'],
|
|
nodes: ['bulletList', 'orderedList', 'codeBlock'],
|
|
menu: [
|
|
'copilot',
|
|
'strong',
|
|
'em',
|
|
'code',
|
|
'strike',
|
|
'bulletList',
|
|
'orderedList',
|
|
'undo',
|
|
'redo',
|
|
],
|
|
},
|
|
'Channel::Line': {
|
|
marks: ['strong', 'em', 'code', 'strike'],
|
|
nodes: ['codeBlock'],
|
|
menu: ['copilot', 'strong', 'em', 'code', 'strike', 'undo', 'redo'],
|
|
},
|
|
'Channel::Telegram': {
|
|
marks: ['strong', 'em', 'link', 'code'],
|
|
nodes: [],
|
|
menu: ['copilot', 'strong', 'em', 'link', 'code', 'undo', 'redo'],
|
|
},
|
|
'Channel::Instagram': {
|
|
marks: ['strong', 'em', 'code', 'strike'],
|
|
nodes: ['bulletList', 'orderedList'],
|
|
menu: [
|
|
'copilot',
|
|
'strong',
|
|
'em',
|
|
'code',
|
|
'bulletList',
|
|
'orderedList',
|
|
'strike',
|
|
'undo',
|
|
'redo',
|
|
],
|
|
},
|
|
'Channel::Tiktok': {
|
|
marks: [],
|
|
nodes: [],
|
|
menu: [],
|
|
},
|
|
// Special contexts (not actual channels)
|
|
'Context::PrivateNote': {
|
|
marks: ['strong', 'em', 'code', 'link', 'strike'],
|
|
nodes: ['bulletList', 'orderedList', 'codeBlock', 'blockquote'],
|
|
menu: [
|
|
'copilot',
|
|
'strong',
|
|
'em',
|
|
'code',
|
|
'link',
|
|
'strike',
|
|
'bulletList',
|
|
'orderedList',
|
|
'undo',
|
|
'redo',
|
|
],
|
|
},
|
|
'Context::Default': {
|
|
marks: ['strong', 'em', 'code', 'link', 'strike'],
|
|
nodes: ['bulletList', 'orderedList', 'codeBlock', 'blockquote'],
|
|
menu: [
|
|
'strong',
|
|
'em',
|
|
'code',
|
|
'link',
|
|
'strike',
|
|
'bulletList',
|
|
'orderedList',
|
|
'undo',
|
|
'redo',
|
|
],
|
|
},
|
|
'Context::MessageSignature': {
|
|
marks: ['strong', 'em', 'link'],
|
|
nodes: ['image'],
|
|
menu: ['strong', 'em', 'link', 'undo', 'redo', 'imageUpload'],
|
|
},
|
|
'Context::InboxSettings': {
|
|
marks: ['strong', 'em', 'link'],
|
|
nodes: [],
|
|
menu: ['strong', 'em', 'link', 'undo', 'redo'],
|
|
},
|
|
'Context::Plain': {
|
|
marks: [],
|
|
nodes: [],
|
|
menu: [],
|
|
},
|
|
};
|
|
|
|
// Editor menu options for Full Editor
|
|
export const ARTICLE_EDITOR_MENU_OPTIONS = [
|
|
'strong',
|
|
'em',
|
|
'strike',
|
|
'link',
|
|
'undo',
|
|
'redo',
|
|
'bulletList',
|
|
'orderedList',
|
|
'h1',
|
|
'h2',
|
|
'h3',
|
|
'imageUpload',
|
|
'code',
|
|
'insertTable',
|
|
];
|
|
|
|
/**
|
|
* Markdown formatting patterns for stripping unsupported formatting.
|
|
*
|
|
* Maps camelCase type names to ProseMirror snake_case schema names.
|
|
* Order matters: codeBlock before code to avoid partial matches.
|
|
*/
|
|
export const MARKDOWN_PATTERNS = [
|
|
// --- BLOCK NODES ---
|
|
{
|
|
type: 'codeBlock', // PM: code_block, eg: ```js\ncode\n```
|
|
patterns: [
|
|
{ pattern: /`{3}(?:\w+)?\n?([\s\S]*?)`{3}/g, replacement: '$1' },
|
|
],
|
|
},
|
|
{
|
|
type: 'blockquote', // PM: blockquote, eg: > quote
|
|
patterns: [{ pattern: /^> ?/gm, replacement: '' }],
|
|
},
|
|
{
|
|
type: 'bulletList', // PM: bullet_list, eg: - item
|
|
patterns: [{ pattern: /^[\t ]*[-*+]\s+/gm, replacement: '' }],
|
|
},
|
|
{
|
|
type: 'orderedList', // PM: ordered_list, eg: 1. item
|
|
patterns: [{ pattern: /^[\t ]*\d+\.\s+/gm, replacement: '' }],
|
|
},
|
|
{
|
|
type: 'heading', // PM: heading, eg: ## Heading
|
|
patterns: [{ pattern: /^#{1,6}\s+/gm, replacement: '' }],
|
|
},
|
|
{
|
|
type: 'horizontalRule', // PM: horizontal_rule, eg: ---
|
|
patterns: [{ pattern: /^(?:---|___|\*\*\*)\s*$/gm, replacement: '' }],
|
|
},
|
|
{
|
|
type: 'image', // PM: image, eg: 
|
|
patterns: [{ pattern: /!\[([^\]]*)\]\([^)]+\)/g, replacement: '$1' }],
|
|
},
|
|
{
|
|
type: 'hardBreak', // PM: hard_break, eg: line\\\n or line \n
|
|
patterns: [
|
|
{ pattern: /\\\n/g, replacement: '\n' },
|
|
{ pattern: / {2,}\n/g, replacement: '\n' },
|
|
],
|
|
},
|
|
// --- INLINE MARKS ---
|
|
{
|
|
type: 'strong', // PM: strong, eg: **bold** or __bold__
|
|
patterns: [
|
|
{ pattern: /\*\*(.+?)\*\*/g, replacement: '$1' },
|
|
{ pattern: /__(.+?)__/g, replacement: '$1' },
|
|
],
|
|
},
|
|
{
|
|
type: 'em', // PM: em, eg: *italic* or _italic_
|
|
patterns: [
|
|
{ pattern: /(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, replacement: '$1' },
|
|
// Match _text_ only at word boundaries (whitespace/string start/end)
|
|
// Preserves underscores in URLs (e.g., https://example.com/path_name) and variable names
|
|
{
|
|
pattern: /(?<=^|[\s])_([^_\s][^_]*[^_\s]|[^_\s])_(?=$|[\s])/g,
|
|
replacement: '$1',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
type: 'strike', // PM: strike, eg: ~~strikethrough~~
|
|
patterns: [{ pattern: /~~(.+?)~~/g, replacement: '$1' }],
|
|
},
|
|
{
|
|
type: 'code', // PM: code, eg: `inline code`
|
|
patterns: [{ pattern: /`([^`]+)`/g, replacement: '$1' }],
|
|
},
|
|
{
|
|
type: 'link', // PM: link
|
|
patterns: [
|
|
{ pattern: /\[([^\]]+)\]\([^)]+\)/g, replacement: '$1' }, // [text](url) -> text
|
|
{ pattern: /<([a-zA-Z][a-zA-Z0-9+.-]*:[^\s>]+)>/g, replacement: '$1' }, // <https://...>, <mailto:...>, <tel:...>, <ftp://...>, etc
|
|
{ pattern: /<([^\s@]+@[^\s@>]+)>/g, replacement: '$1' }, // <[email protected]> -> [email protected]
|
|
],
|
|
},
|
|
];
|
|
|
|
// Editor image resize options for Message Editor
|
|
export const MESSAGE_EDITOR_IMAGE_RESIZES = [
|
|
{
|
|
name: 'Small',
|
|
height: '24px',
|
|
},
|
|
{
|
|
name: 'Medium',
|
|
height: '48px',
|
|
},
|
|
{
|
|
name: 'Large',
|
|
height: '72px',
|
|
},
|
|
{
|
|
name: 'Original Size',
|
|
height: 'auto',
|
|
},
|
|
];
|