diff --git a/app/operator_panel/app_config.php b/app/operator_panel/app_config.php index 7763d09d83..89a1dd28e0 100644 --- a/app/operator_panel/app_config.php +++ b/app/operator_panel/app_config.php @@ -237,6 +237,94 @@ $apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true"; $apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for connecting status."; $y++; + $apps[$x]['default_settings'][$y]['default_setting_uuid'] = "ec8f0281-5d2b-4a3b-9481-f2c9880db101"; + $apps[$x]['default_settings'][$y]['default_setting_category'] = "theme"; + $apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "operator_panel_conference_icon_mute"; + $apps[$x]['default_settings'][$y]['default_setting_name'] = "text"; + $apps[$x]['default_settings'][$y]['default_setting_value'] = "fas fa-microphone"; + $apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true"; + $apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for the conference mute action."; + $y++; + $apps[$x]['default_settings'][$y]['default_setting_uuid'] = "3d4dbe79-f1af-4bea-804e-27ee0ac31302"; + $apps[$x]['default_settings'][$y]['default_setting_category'] = "theme"; + $apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "operator_panel_conference_icon_unmute"; + $apps[$x]['default_settings'][$y]['default_setting_name'] = "text"; + $apps[$x]['default_settings'][$y]['default_setting_value'] = "fas fa-microphone-slash"; + $apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true"; + $apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for the conference unmute action."; + $y++; + $apps[$x]['default_settings'][$y]['default_setting_uuid'] = "b15d1ae4-6087-48cf-a46f-148fb5236103"; + $apps[$x]['default_settings'][$y]['default_setting_category'] = "theme"; + $apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "operator_panel_conference_icon_deaf"; + $apps[$x]['default_settings'][$y]['default_setting_name'] = "text"; + $apps[$x]['default_settings'][$y]['default_setting_value'] = "fas fa-headphones"; + $apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true"; + $apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for the conference deaf action."; + $y++; + $apps[$x]['default_settings'][$y]['default_setting_uuid'] = "2b560cff-58ef-42c2-b4e5-08d77c339a04"; + $apps[$x]['default_settings'][$y]['default_setting_category'] = "theme"; + $apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "operator_panel_conference_icon_undeaf"; + $apps[$x]['default_settings'][$y]['default_setting_name'] = "text"; + $apps[$x]['default_settings'][$y]['default_setting_value'] = "fas fa-deaf"; + $apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true"; + $apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for the conference undeaf action."; + $y++; + $apps[$x]['default_settings'][$y]['default_setting_uuid'] = "21f8af30-d08f-4342-bddb-bb68804e1005"; + $apps[$x]['default_settings'][$y]['default_setting_category'] = "theme"; + $apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "operator_panel_conference_icon_energy_up"; + $apps[$x]['default_settings'][$y]['default_setting_name'] = "text"; + $apps[$x]['default_settings'][$y]['default_setting_value'] = "fas fa-plus"; + $apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true"; + $apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for the conference energy up action."; + $y++; + $apps[$x]['default_settings'][$y]['default_setting_uuid'] = "4fdc30b2-7db8-4c9f-9b7e-4c78fa757706"; + $apps[$x]['default_settings'][$y]['default_setting_category'] = "theme"; + $apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "operator_panel_conference_icon_energy_down"; + $apps[$x]['default_settings'][$y]['default_setting_name'] = "text"; + $apps[$x]['default_settings'][$y]['default_setting_value'] = "fas fa-minus"; + $apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true"; + $apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for the conference energy down action."; + $y++; + $apps[$x]['default_settings'][$y]['default_setting_uuid'] = "2b706528-8a6b-409c-84bb-95f905344707"; + $apps[$x]['default_settings'][$y]['default_setting_category'] = "theme"; + $apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "operator_panel_conference_icon_volume_down"; + $apps[$x]['default_settings'][$y]['default_setting_name'] = "text"; + $apps[$x]['default_settings'][$y]['default_setting_value'] = "fas fa-volume-down"; + $apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true"; + $apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for the conference volume down action."; + $y++; + $apps[$x]['default_settings'][$y]['default_setting_uuid'] = "9220ec76-6760-4b75-a29e-503b94ca7508"; + $apps[$x]['default_settings'][$y]['default_setting_category'] = "theme"; + $apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "operator_panel_conference_icon_volume_up"; + $apps[$x]['default_settings'][$y]['default_setting_name'] = "text"; + $apps[$x]['default_settings'][$y]['default_setting_value'] = "fas fa-volume-up"; + $apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true"; + $apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for the conference volume up action."; + $y++; + $apps[$x]['default_settings'][$y]['default_setting_uuid'] = "866dc078-68cb-43aa-b70b-b73d213dbd09"; + $apps[$x]['default_settings'][$y]['default_setting_category'] = "theme"; + $apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "operator_panel_conference_icon_gain_down"; + $apps[$x]['default_settings'][$y]['default_setting_name'] = "text"; + $apps[$x]['default_settings'][$y]['default_setting_value'] = "fas fa-sort-amount-down"; + $apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true"; + $apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for the conference gain down action."; + $y++; + $apps[$x]['default_settings'][$y]['default_setting_uuid'] = "d4bde1f1-b0d8-4836-a78b-f8c4bb510a10"; + $apps[$x]['default_settings'][$y]['default_setting_category'] = "theme"; + $apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "operator_panel_conference_icon_gain_up"; + $apps[$x]['default_settings'][$y]['default_setting_name'] = "text"; + $apps[$x]['default_settings'][$y]['default_setting_value'] = "fas fa-sort-amount-up"; + $apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true"; + $apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for the conference gain up action."; + $y++; + $apps[$x]['default_settings'][$y]['default_setting_uuid'] = "f55d2796-46b5-4ca2-998f-256184489b11"; + $apps[$x]['default_settings'][$y]['default_setting_category'] = "theme"; + $apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "operator_panel_conference_icon_kick"; + $apps[$x]['default_settings'][$y]['default_setting_name'] = "text"; + $apps[$x]['default_settings'][$y]['default_setting_value'] = "fas fa-ban"; + $apps[$x]['default_settings'][$y]['default_setting_enabled'] = "true"; + $apps[$x]['default_settings'][$y]['default_setting_description'] = "Font Awesome icon class for the conference kick action."; + $y++; $apps[$x]['default_settings'][$y]['default_setting_uuid'] = "b367ce0d-beb0-424a-9fc9-0c83690001ee"; $apps[$x]['default_settings'][$y]['default_setting_category'] = "theme"; $apps[$x]['default_settings'][$y]['default_setting_subcategory'] = "operator_panel_status_show_icon"; diff --git a/app/operator_panel/index.php b/app/operator_panel/index.php index cc3cd95515..9579a776fa 100644 --- a/app/operator_panel/index.php +++ b/app/operator_panel/index.php @@ -81,6 +81,19 @@ 'disconnected' => $settings->get('theme', 'operator_panel_status_icon_disconnected', 'fa-solid fa-plug-circle-xmark'), 'connecting' => $settings->get('theme', 'operator_panel_status_icon_connecting', 'fa-solid fa-plug fa-fade'), ]; + $conference_action_icons = [ + 'mute' => $settings->get('theme', 'operator_panel_conference_icon_mute', 'fas fa-microphone'), + 'unmute' => $settings->get('theme', 'operator_panel_conference_icon_unmute', 'fas fa-microphone-slash'), + 'deaf' => $settings->get('theme', 'operator_panel_conference_icon_deaf', 'fas fa-headphones'), + 'undeaf' => $settings->get('theme', 'operator_panel_conference_icon_undeaf', 'fas fa-deaf'), + 'energy_up' => $settings->get('theme', 'operator_panel_conference_icon_energy_up', 'fas fa-plus'), + 'energy_down' => $settings->get('theme', 'operator_panel_conference_icon_energy_down', 'fas fa-minus'), + 'volume_down' => $settings->get('theme', 'operator_panel_conference_icon_volume_down', 'fas fa-volume-down'), + 'volume_up' => $settings->get('theme', 'operator_panel_conference_icon_volume_up', 'fas fa-volume-up'), + 'gain_down' => $settings->get('theme', 'operator_panel_conference_icon_gain_down', 'fas fa-sort-amount-down'), + 'gain_up' => $settings->get('theme', 'operator_panel_conference_icon_gain_up', 'fas fa-sort-amount-up'), + 'kick' => $settings->get('theme', 'operator_panel_conference_icon_kick', 'fas fa-ban'), + ]; $status_show_icon = $settings->get('theme', 'operator_panel_status_show_icon', 'true') === 'true'; // Optional user status list for the presence dropdown @@ -127,6 +140,7 @@ // Theme colors and icons for connection status indicator const status_colors = ; const status_icons = ; + const conference_action_icons = ; const status_tooltips = { connected: , warning: , diff --git a/app/operator_panel/resources/classes/operator_panel_conf_filter.php b/app/operator_panel/resources/classes/operator_panel_conf_filter.php index 4d2836dfd2..5419ac872c 100644 --- a/app/operator_panel/resources/classes/operator_panel_conf_filter.php +++ b/app/operator_panel/resources/classes/operator_panel_conf_filter.php @@ -27,33 +27,46 @@ */ /** - * Filters conference events to only include keys relevant to the operator panel, - * and drops messages from domains other than the subscriber's. + * Filters conference events using conference-aware domain detection. * - * Passes only the keys listed in {@see operator_panel_service::conf_event_keys} - * and enforces domain isolation using caller_context. - * - * @author Tim Fry + * Conference maintenance events are not consistent about which field carries + * the domain. Accept a message when the domain can be proven from either the + * conference identifier or the caller context, and otherwise keep the message + * so operator panel conference updates are not dropped. */ class operator_panel_conf_filter implements filter { /** - * Allowed domain names keyed for fast lookup + * Allowed domain names keyed for fast lookup. * * @var array */ - private $domains; + private $domains = []; /** - * Keys that are permitted through the filter + * Keys that are permitted through the filter. * * @var array */ - private $allowed_keys; + private $allowed_keys = []; /** - * @param array $domain_names Domain names this subscriber is allowed to see. - * @param array $allowed_keys Event keys to include in the forwarded payload. + * Whether the current event should be dropped. + * + * @var bool + */ + private $drop_message = false; + + /** + * Whether a matching domain has been positively identified. + * + * @var bool + */ + private $matched_domain = false; + + /** + * @param array $domain_names Domain names this subscriber is allowed to see. + * @param array $allowed_keys Event keys to include in the forwarded payload. */ public function __construct(array $domain_names, array $allowed_keys) { foreach ($domain_names as $name) { @@ -68,15 +81,69 @@ class operator_panel_conf_filter implements filter { * @param string $key * @param mixed $value * - * @return bool|null true to keep, false to skip this key, null to drop the entire message. + * @return bool|null true to keep, false to skip this key, null to drop the entire message. */ public function __invoke($key, $value): ?bool { - // Domain guard — drop whole message if context is wrong - if ($key === 'caller_context') { - return isset($this->domains[$value]) ? true : null; + if ($this->drop_message) { + return null; } - // Key allow-list - return isset($this->allowed_keys[$key]) ? true : false; + if ($this->is_domain_key($key)) { + $matched = $this->match_domain((string)$value, $key === 'caller_context'); + if ($matched === false) { + $this->drop_message = true; + return null; + } + } + + if ($this->matched_domain || !$this->is_domain_key($key)) { + return isset($this->allowed_keys[$key]) ? true : false; + } + + return true; + } + + /** + * Determine whether the key can identify the conference domain. + * + * @param string $key + * + * @return bool + */ + private function is_domain_key(string $key): bool { + return in_array($key, ['caller_context', 'conference_name', 'channel_presence_id'], true); + } + + /** + * Match the event's domain against the allowed set. + * + * @param string $value + * @param bool $is_context + * + * @return bool|null false when the event belongs to another domain, true when it matches, + * or null when the key does not conclusively identify a domain. + */ + private function match_domain(string $value, bool $is_context): ?bool { + $value = trim($value); + if ($value === '') { + return null; + } + + $domain = $value; + if (!$is_context && strpos($value, '@') !== false) { + $parts = explode('@', $value); + $domain = end($parts) ?: ''; + } + + if ($domain === '') { + return null; + } + + if (isset($this->domains[$domain])) { + $this->matched_domain = true; + return true; + } + + return false; } } diff --git a/app/operator_panel/resources/classes/operator_panel_service.php b/app/operator_panel/resources/classes/operator_panel_service.php index 731b6487bf..462500b416 100644 --- a/app/operator_panel/resources/classes/operator_panel_service.php +++ b/app/operator_panel/resources/classes/operator_panel_service.php @@ -180,6 +180,15 @@ class operator_panel_service extends base_websocket_system_service implements we 'recording_state' => 'operator_panel_record', 'registrations_state' => 'operator_panel_view', 'originate' => 'operator_panel_originate', + // Conference member actions + 'mute' => 'operator_panel_manage', + 'unmute' => 'operator_panel_manage', + 'deaf' => 'operator_panel_manage', + 'undeaf' => 'operator_panel_manage', + 'energy' => 'operator_panel_manage', + 'volume_in' => 'operator_panel_manage', + 'volume_out' => 'operator_panel_manage', + 'kick' => 'operator_panel_hangup', // User presence status (own status only, enforced inside handler) 'user_status' => 'operator_panel_view', // Call-center agent status (supervisor action) @@ -854,6 +863,9 @@ class operator_panel_service extends base_websocket_system_service implements we $destination = $payload['destination'] ?? ''; $domain_name = $payload['domain_name'] ?? ''; $context = $payload['context'] ?? ($domain_name !== '' ? $domain_name : 'default'); + $conference_name = html_entity_decode(urldecode($payload['conference_name'] ?? '')); + $member_id = $payload['member_id'] ?? ''; + $direction = $payload['direction'] ?? ''; try { switch ($action) { @@ -993,6 +1005,69 @@ class operator_panel_service extends base_websocket_system_service implements we } return ['success' => true, 'message' => 'Registrations state updated', 'states' => $states]; + case 'mute': + case 'unmute': + if (empty($conference_name) || strpos($conference_name, '@') === false) { + return ['success' => false, 'message' => 'Invalid conference name']; + } + if (empty($member_id)) { + return ['success' => false, 'message' => 'Member ID required']; + } + event_socket::api("conference '$conference_name' $action $member_id"); + if (!empty($uuid)) { + event_socket::api("uuid_setvar $uuid hand_raised false"); + } + return ['success' => true, 'message' => 'Conference member updated']; + + case 'deaf': + case 'undeaf': + if (empty($conference_name) || strpos($conference_name, '@') === false) { + return ['success' => false, 'message' => 'Invalid conference name']; + } + if (empty($member_id)) { + return ['success' => false, 'message' => 'Member ID required']; + } + event_socket::api("conference '$conference_name' $action $member_id"); + return ['success' => true, 'message' => 'Conference member updated']; + + case 'kick': + if (empty($uuid)) { + return ['success' => false, 'message' => 'UUID required']; + } + event_socket::api("uuid_kill $uuid"); + return ['success' => true, 'message' => 'Conference member removed']; + + case 'energy': + if (empty($conference_name) || strpos($conference_name, '@') === false) { + return ['success' => false, 'message' => 'Invalid conference name']; + } + if (empty($member_id) || empty($direction)) { + return ['success' => false, 'message' => 'Member ID and direction required']; + } + $current = trim((string) event_socket::api("conference '$conference_name' energy $member_id")); + if (preg_match('/=(\d+)/', $current, $matches)) { + $value = (int) $matches[1]; + $value = ($direction === 'up') ? $value + 100 : $value - 100; + event_socket::api("conference '$conference_name' energy $member_id $value"); + } + return ['success' => true, 'message' => 'Energy updated']; + + case 'volume_in': + case 'volume_out': + if (empty($conference_name) || strpos($conference_name, '@') === false) { + return ['success' => false, 'message' => 'Invalid conference name']; + } + if (empty($member_id) || empty($direction)) { + return ['success' => false, 'message' => 'Member ID and direction required']; + } + $current = trim((string) event_socket::api("conference '$conference_name' $action $member_id")); + if (preg_match('/=(-?\d+)/', $current, $matches)) { + $value = (int) $matches[1]; + $value = ($direction === 'up') ? $value + 1 : $value - 1; + event_socket::api("conference '$conference_name' $action $member_id $value"); + } + return ['success' => true, 'message' => 'Volume updated']; + case 'originate': $source = $payload['source'] ?? ''; $dest = $payload['destination'] ?? ''; @@ -1282,7 +1357,7 @@ class operator_panel_service extends base_websocket_system_service implements we case 'energy_level': case 'gain_level': case 'volume_level': - $this->broadcast_call_event($event_message, $action); + $this->broadcast_conference_event($event_message, $action, $conference_name); break; case 'add_member': @@ -1445,6 +1520,53 @@ class operator_panel_service extends base_websocket_system_service implements we websocket_client::send($this->ws_client->socket(), $message); } + /** + * Broadcast a conference event with a normalized conference identity. + * + * @param event_message $event_message + * @param string $action + * @param string $conference_name + * + * @return void + */ + private function broadcast_conference_event(event_message $event_message, string $action, string $conference_name): void { + $event_data = $event_message->to_array(); + $event_data = $this->normalize_conference_payload($event_data, $conference_name); + + + $message = new websocket_message(); + $message + ->service_name(self::get_service_name()) + ->topic($action) + ->payload($event_data) + ; + websocket_client::send($this->ws_client->socket(), $message); + } + + /** + * Add a normalized conference identity and context to an event payload. + * + * @param array $event_data + * @param string $conference_name + * + * @return array + */ + private function normalize_conference_payload(array $event_data, string $conference_name): array { + $event_data['conference_name'] = $conference_name; + $event_data['conference_display_name'] = $this->lookup_conference_display_name($conference_name); + + if (strpos($conference_name, '@') !== false) { + $parts = explode('@', $conference_name, 2); + $domain_name = $parts[1] ?? ''; + if ($domain_name !== '') { + $event_data['domain_name'] = $domain_name; + $event_data['caller_context'] = $domain_name; + } + } + + return $event_data; + } + /** * Send an action response back to the requesting client. * @@ -1521,9 +1643,7 @@ class operator_panel_service extends base_websocket_system_service implements we ]; } - $event_data['conference_name'] = $conference_name; - $event_data['conference_display_name'] = $this->lookup_conference_display_name($conference_name); - return $event_data; + return $this->normalize_conference_payload($event_data, $conference_name); } /** @@ -1546,10 +1666,8 @@ class operator_panel_service extends base_websocket_system_service implements we : 0; } - $event_data['member_count'] = $member_count ?? 0; - $event_data['conference_name'] = $conference_name; - $event_data['conference_display_name'] = $this->lookup_conference_display_name($conference_name); - return $event_data; + $event_data['member_count'] = $member_count ?? 0; + return $this->normalize_conference_payload($event_data, $conference_name); } /** @@ -1562,10 +1680,8 @@ class operator_panel_service extends base_websocket_system_service implements we */ private function enrich_conference_create_event(event_message $event_message, string $conference_name): array { $event_data = $event_message->to_array(); - $event_data['conference_name'] = $conference_name; - $event_data['conference_display_name'] = $this->lookup_conference_display_name($conference_name); $event_data['member_count'] = 0; - return $event_data; + return $this->normalize_conference_payload($event_data, $conference_name); } /** diff --git a/app/operator_panel/resources/javascript/operator_panel.js b/app/operator_panel/resources/javascript/operator_panel.js index a584d5c6dd..526267c5fb 100644 --- a/app/operator_panel/resources/javascript/operator_panel.js +++ b/app/operator_panel/resources/javascript/operator_panel.js @@ -496,6 +496,17 @@ function esc(text) { .replace(/'/g, '''); } +function jsq(value) { + return JSON.stringify(value === null || value === undefined ? '' : String(value)); +} + +function get_conference_action_icon(name, fallback) { + if (conference_action_icons && conference_action_icons[name]) { + return conference_action_icons[name]; + } + return fallback; +} + /** Format a Unix microsecond timestamp as elapsed time hh:mm:ss */ function format_elapsed(us_timestamp) { if (!us_timestamp || us_timestamp === '0') return '--:--:--'; @@ -571,7 +582,7 @@ function on_calls_snapshot_event(event) { * @param {object} event */ function on_call_event(event) { - const name = (event.event_name || event.topic || '').toLowerCase(); + const name = (event.topic || event.event_name || '').toLowerCase(); const uuid = get_call_uuid(event); if (!uuid) return; @@ -881,7 +892,7 @@ function load_conferences_snapshot() { * @param {object} event */ function on_conference_event(event) { - const action = (event.event_name || event.topic || '').toLowerCase(); + const action = (event.topic || event.event_name || '').toLowerCase(); const conference_name = event.conference_name || event.channel_presence_id || ''; if (!conference_name) return; @@ -1023,32 +1034,57 @@ function render_conferences_tab() { html += ` \n`; members.forEach(m => { + const conf_name_js = jsq(name); + const member_id_js = jsq(String(m.id || '')); + const uuid_js = jsq(m.uuid || ''); const mid = esc(String(m.id || '')); const muuid = esc(m.uuid || ''); const cid = esc(m.caller_id_name || ''); const cid_num = esc(m.caller_id_number || ''); const flags = m.flags || {}; + let flags_html = ''; + + if (flags.talking) { + flags_html += ` ${esc(text['label-talking'] || 'Talking')}`; + } + if (flags.can_speak === false) { + flags_html += ` ${esc(text['label-muted'] || 'Muted')}`; + } + if (flags.can_hear === false) { + flags_html += ` ${esc(text['label-deaf'] || 'Deaf')}`; + } + if (flags.has_floor) { + flags_html += ` ${esc(text['label-floor'] || 'Floor')}`; + } + if (flags.is_moderator) { + flags_html += ` ${esc(text['label-moderator'] || 'Moderator')}`; + } html += ` \n`; html += ` ${cid ? `${cid}
${cid_num}` : cid_num}\n`; html += ` ${mid}\n`; html += ` `; - html += ` ${esc(text['label-talking'] || 'Talking')}`; - html += ` ${esc(text['label-muted'] || 'Muted')}`; - html += ` ${esc(text['label-deaf'] || 'Deaf')}`; - if (flags.has_floor) html += ` ${esc(text['label-floor'] || 'Floor')}`; - if (flags.is_moderator) html += ` ${esc(text['label-moderator'] || 'Moderator')}`; + html += flags_html; html += ` \n`; if (permissions.operator_panel_hangup || permissions.operator_panel_manage) { html += ` \n`; if (permissions.operator_panel_hangup) { - html += ` ` - + `${esc(text['button-hangup'] || 'Hangup')} `; + html += ` `; } if (permissions.operator_panel_manage) { - html += ` ` - + `${esc(text['button-transfer'] || 'Transfer')} `; + const mute_action = flags.can_speak === false ? 'unmute' : 'mute'; + const mute_label = flags.can_speak === false ? (text['button-unmute'] || 'Unmute') : (text['button-mute'] || 'Mute'); + const deaf_action = flags.can_hear === false ? 'undeaf' : 'deaf'; + const deaf_label = flags.can_hear === false ? (text['button-undeaf'] || 'Undeaf') : (text['button-deaf'] || 'Deaf'); + html += ` `; + html += ` `; + html += ` `; + html += ` `; + html += ` `; + html += ` `; + html += ` `; + html += ` `; } html += ` \n`; } @@ -1351,6 +1387,22 @@ function action_agent_status(agent_name, status) { send_action('agent_status', { agent_name, status }).catch(console.error); } +function action_conference_member(action, conference_name, member_id, uuid, direction) { + const payload = { conference_name, member_id, uuid }; + if (direction) { + payload.direction = direction; + } + send_action(action, payload) + .then(() => { + if (action === 'kick') { + show_toast(text['button-hangup'] || 'Member removed', 'success'); + return; + } + show_toast(text['label-actions'] || 'Action executed', 'success'); + }) + .catch(console.error); +} + /** * Show a brief Bootstrap toast notification. * @param {string} message