diff --git a/include/class.config.php b/include/class.config.php index b3c75485c1ed9d8862656cb4e9c2e8ea1d1dc20f..8993ff264bed9decf7adb3768e30b787a934679d 100644 --- a/include/class.config.php +++ b/include/class.config.php @@ -29,13 +29,16 @@ class Config { # new settings and the corresponding default values. var $defaults = array(); # List of default values - function Config($section=null) { + function Config($section=null, $defaults=array()) { if ($section) $this->section = $section; if ($this->section === null) return false; + if ($defaults) + $this->defaults = $defaults; + if (isset($_SESSION['cfg:'.$this->section])) $this->session = &$_SESSION['cfg:'.$this->section]; $this->load(); @@ -1136,6 +1139,7 @@ class OsticketConfig extends Config { 'allow_pw_reset'=>isset($vars['allow_pw_reset'])?1:0, 'pw_reset_window'=>$vars['pw_reset_window'], 'agent_name_format'=>$vars['agent_name_format'], + 'hide_staff_name'=>isset($vars['hide_staff_name']) ? 1 : 0, 'agent_avatar'=>$vars['agent_avatar'], )); } @@ -1207,7 +1211,6 @@ class OsticketConfig extends Config { 'show_assigned_tickets'=>isset($vars['show_assigned_tickets'])?0:1, 'show_answered_tickets'=>isset($vars['show_answered_tickets'])?0:1, 'show_related_tickets'=>isset($vars['show_related_tickets'])?1:0, - 'hide_staff_name'=>isset($vars['hide_staff_name'])?1:0, 'allow_client_updates'=>isset($vars['allow_client_updates'])?1:0, 'ticket_lock' => $vars['ticket_lock'], )); diff --git a/include/class.mailer.php b/include/class.mailer.php index 3faae326690d2fe129549da8d4594b4041c0a5e3..1d3712211a43a69d040b976a4fa80d67899b99bc 100644 --- a/include/class.mailer.php +++ b/include/class.mailer.php @@ -66,10 +66,14 @@ class Mailer { $this->ht['from'] = $from; } - function getFromAddress() { + function getFromAddress($options=array()) { - if(!$this->ht['from'] && ($email=$this->getEmail())) - $this->ht['from'] =sprintf('"%s" <%s>', ($email->getName()?$email->getName():$email->getEmail()), $email->getEmail()); + if (!$this->ht['from'] && ($email=$this->getEmail())) { + if (($name = $options['from_name'] ?: $email->getName())) + $this->ht['from'] =sprintf('"%s" <%s>', $name, $email->getEmail()); + else + $this->ht['from'] =sprintf('<%s>', $email->getEmail()); + } return $this->ht['from']; } @@ -318,7 +322,7 @@ class Mailer { $subject = preg_replace("/(\r\n|\r|\n)/s",'', trim($subject)); $headers = array ( - 'From' => $this->getFromAddress(), + 'From' => $this->getFromAddress($options), 'To' => $to, 'Subject' => $subject, 'Date'=> date('D, d M Y H:i:s O'), diff --git a/include/class.staff.php b/include/class.staff.php index cbbc5200f3fc3cebb8ec7df6f067c26ee753e11c..e3273fae5c5e0b3715b90905944caa0327377818 100644 --- a/include/class.staff.php +++ b/include/class.staff.php @@ -51,6 +51,7 @@ implements AuthenticatedUser, EmailContact, TemplateVariable { var $passwd_change; var $_roles = null; var $_teams = null; + var $_config = null; var $_perm; function __onload() { @@ -66,6 +67,34 @@ implements AuthenticatedUser, EmailContact, TemplateVariable { $this->passwd_change = time()-$time; //XXX: check timezone issues. } + function get($field, $default=false) { + + // Autoload config if not loaded already + if (!isset($this->_config)) + $this->getConfig(); + + if (isset($this->_config[$field])) + return $this->_config[$field]; + + return parent::get($field, $default); + } + + function getConfig() { + + if (!isset($this->_config) && $this->getId()) { + $_config = new Config('staff.'.$this->getId(), + // Defaults + array( + 'default_from_name' => '', + 'datetime_format' => '', + 'thread_view_order' => '', + )); + $this->_config = $_config->getInfo(); + } + + return $this->_config; + } + function __toString() { return (string) $this->getName(); } @@ -102,6 +131,10 @@ implements AuthenticatedUser, EmailContact, TemplateVariable { $base = $this->ht; unset($base['teams']); unset($base['dept_access']); + + if ($this->getConfig()) + $base += $this->getConfig(); + return $base; } @@ -282,6 +315,10 @@ implements AuthenticatedUser, EmailContact, TemplateVariable { return $this->default_signature_type; } + function getReplyFromNameType() { + return $this->default_from_name; + } + function getDefaultPaperSize() { return $this->default_paper_size; } @@ -623,7 +660,7 @@ implements AuthenticatedUser, EmailContact, TemplateVariable { $this->default_signature_type = $vars['default_signature_type']; $this->default_paper_size = $vars['default_paper_size']; $this->lang = $vars['lang']; - $this->onvacation = isset($vars['onvacation'])?1:0; + $this->onvacation = isset($vars['onvacation']) ? 1 : 0; if (isset($vars['avatar_code'])) $this->setExtraAttr('avatar', $vars['avatar_code']); @@ -634,6 +671,16 @@ implements AuthenticatedUser, EmailContact, TemplateVariable { $_SESSION['::lang'] = null; TextDomain::configureForUser($this); + // Update the config information + $_config = new Config('staff.'.$this->getId()); + $_config->updateAll(array( + 'datetime_format' => $vars['datetime_format'], + 'default_from_name' => $vars['default_from_name'], + 'thread_view_order' => $vars['thread_view_order'], + ) + ); + $this->_config = $_config->getInfo(); + return $this->save(); } diff --git a/include/class.thread.php b/include/class.thread.php index 21ac9336cde18cdf3cfe4631aee13b09e436f731..6abb8a8df4f3a058247a55f7fb610539cd37cc4f 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -244,6 +244,9 @@ class Thread extends VerySimpleModel { if ($type && is_array($type)) $entries->filter(array('type__in' => $type)); + if ($options['sort'] && !strcasecmp($options['sort'], 'DESC')) + $entries->order_by('-id'); + // Precache all the attachments on this thread AttachmentFile::objects()->filter(array( 'attachments__thread_entry__thread__id' => $this->id @@ -1607,9 +1610,10 @@ class ThreadEvent extends VerySimpleModel { } function template($description) { + global $thisstaff; $self = $this; return preg_replace_callback('/\{(<(?P<type>([^>]+))>)?(?P<key>[^}.]+)(\.(?P<data>[^}]+))?\}/', - function ($m) use ($self) { + function ($m) use ($self, $thisstaff) { switch ($m['key']) { case 'assignees': $assignees = array(); @@ -1628,10 +1632,22 @@ class ThreadEvent extends VerySimpleModel { $name = $avatar.$name; return $name; case 'timestamp': - return sprintf('<time class="relative" datetime="%s" title="%s">%s</time>', + $timeFormat = null; + if ($thisstaff + && !strcasecmp($thisstaff->datetime_format, + 'relative')) { + $timeFormat = function ($timestamp) { + return Format::relativeTime(Misc::db2gmtime($timestamp)); + }; + } + + return sprintf('<time %s datetime="%s" + data-toggle="tooltip" title="%s">%s</time>', + $timeFormat ? 'class="relative"' : '', date(DateTime::W3C, Misc::db2gmtime($self->timestamp)), Format::daydatetime($self->timestamp), - Format::relativeTime(Misc::db2gmtime($self->timestamp)) + $timeFormat ? $timeFormat($self->timestamp) : + Format::datetime($self->timestamp) ); case 'agent': $name = $self->agent->getName(); diff --git a/include/class.ticket.php b/include/class.ticket.php index c97ebbd2e4f9e329f20530047283d992255593d2..3d176f05f580afadcfe217d39c664f029f1c7b7f 100644 --- a/include/class.ticket.php +++ b/include/class.ticket.php @@ -1496,6 +1496,10 @@ implements RestrictedAccess, Threadable { $attachments = $cfg->emailAttachments()?$entry->getAttachments():array(); $options = array('thread' => $entry); + + if ($vars['from_name']) + $options += array('from_name' => $vars['from_name']); + foreach ($recipients as $recipient) { // Skip folks who have already been included on this part of // the conversation @@ -2494,15 +2498,33 @@ implements RestrictedAccess, Threadable { if (!$alert) return $response; - $options = array(); $email = $dept->getEmail(); - + $options = array('thread'=>$response); + $signature = $from_name = ''; if ($thisstaff && $vars['signature']=='mine') $signature=$thisstaff->getSignature(); elseif ($vars['signature']=='dept' && $dept->isPublic()) $signature=$dept->getSignature(); - else - $signature=''; + + if ($thisstaff && ($type=$thisstaff->getReplyFromNameType())) { + switch ($type) { + case 'mine': + if (!$cfg->hideStaffName()) + $from_name = (string) $thisstaff->getName(); + break; + case 'dept': + if ($dept->isPublic()) + $from_name = $dept->getName(); + break; + case 'email': + default: + $from_name = $email->getName(); + } + + if ($from_name) + $options += array('from_name' => $from_name); + + } $variables = array( 'response' => $response, @@ -2511,9 +2533,7 @@ implements RestrictedAccess, Threadable { 'poster' => $thisstaff ); - $user = $this->getOwner(); - $options = array('thread' => $response); if (($email=$dept->getEmail()) && ($tpl = $dept->getTemplate()) && ($msg=$tpl->getReplyMsgTemplate()) @@ -2528,7 +2548,9 @@ implements RestrictedAccess, Threadable { if ($vars['emailcollab']) { $this->notifyCollaborators($response, - array('signature' => $signature) + array( + 'signature' => $signature, + 'from_name' => $from_name) ); } return $response; diff --git a/include/client/templates/thread-entry.tmpl.php b/include/client/templates/thread-entry.tmpl.php index 9e42b053a841fbd971eaef0165ae29cc79b4c283..938a6912fc14ac3d904090f7a99cae39f43ef266 100644 --- a/include/client/templates/thread-entry.tmpl.php +++ b/include/client/templates/thread-entry.tmpl.php @@ -25,10 +25,10 @@ if ($user) </div> <?php echo sprintf(__('<b>%s</b> posted %s'), $name, - sprintf('<time class="relative" datetime="%s" title="%s">%s</time>', + sprintf('<time datetime="%s" title="%s">%s</time>', date(DateTime::W3C, Misc::db2gmtime($entry->created)), Format::daydatetime($entry->created), - Format::relativeTime(Misc::db2gmtime($entry->created)) + Format::datetime($entry->created) ) ); ?> <span style="max-width:500px" class="faded title truncate"><?php diff --git a/include/i18n/en_US/help/tips/settings.agents.yaml b/include/i18n/en_US/help/tips/settings.agents.yaml index bf1ba0602ad93c0766dadf3f1dd63b6f22c2833e..5c699b7efcc46211cb0e5286840eb58100c980c2 100644 --- a/include/i18n/en_US/help/tips/settings.agents.yaml +++ b/include/i18n/en_US/help/tips/settings.agents.yaml @@ -21,6 +21,12 @@ agent_name_format: Choose a format for Agents names throughout the system. Email templates will use it for names if no other format is specified. +staff_identity_masking: + title: Staff Identity Masking + content: > + If enabled, this will hide the Agent’s name from the Client during any + communication. + # Authentication settings password_reset: title: Password Expiration Policy diff --git a/include/i18n/en_US/help/tips/settings.ticket.yaml b/include/i18n/en_US/help/tips/settings.ticket.yaml index b54e13e3cb570ff8abd8c14c17720a118f26e7e4..b13bd17c4f3bc9b2739ddb52f02c69c2d3ca497a 100644 --- a/include/i18n/en_US/help/tips/settings.ticket.yaml +++ b/include/i18n/en_US/help/tips/settings.ticket.yaml @@ -108,12 +108,6 @@ answered_tickets: will be included in the <span class="doc-desc-title">Open Tickets Queue</span>. -staff_identity_masking: - title: Staff Identity Masking - content: > - If enabled, this will hide the Agent’s name from the Client during any - communication. - ticket_attachment_settings: title: Ticket Thread Attachments content: > diff --git a/include/staff/profile.inc.php b/include/staff/profile.inc.php index a41e31304dc72a3ecbc732e7dba3e9bb59bb02f6..e71a1d1e0a10383b297b54e5cb4cc8c262654956 100644 --- a/include/staff/profile.inc.php +++ b/include/staff/profile.inc.php @@ -196,9 +196,59 @@ if ($avatar->isChangeable()) { ?> </select> </td> </tr> + + <tr> + <td><?php echo __('Default From Name');?>: + <div class="faded"><?php echo __('From name to use when replying to a thread');?></div> + </td> + <td> + <select name="default_from_name"> + <?php + $options=array( + 'email' => __("Email Address Name"), + 'dept' => sprintf(__("Department Name (%s)"), + __('if public' /* This is used in 'Department's Name (>if public<)' */)), + 'mine' => __('My Name'), + '' => '— '.__('System Default').' —', + ); + if ($cfg->hideStaffName()) + unset($options['mine']); + + foreach($options as $k=>$v) { + echo sprintf('<option value="%s" %s>%s</option>', + $k,($staff->default_from_name==$k)?'selected="selected"':'',$v); + } + ?> + </select> + <div class="error"><?php echo $errors['default_from_name']; ?></div> + </td> + </tr> + <tr> + <td><?php echo __('Thread View Order');?>: + <div class="faded"><?php echo __('The order of thread entries');?></div> + </td> + <td> + <select name="thread_view_order"> + <?php + $options=array( + 'desc' => __('Descending'), + 'asc' => __('Ascending'), + '' => '— '.__('System Default').' —', + ); + foreach($options as $k=>$v) { + echo sprintf('<option value="%s" %s>%s</option>', + $k + ,($staff->thread_view_order == $k) ? 'selected="selected"' : '' + ,$v); + } + ?> + </select> + <div class="error"><?php echo $errors['thread_view_order']; ?></div> + </td> + </tr> <tr> <td><?php echo __('Default Signature');?>: - <div class="faded"><?php echo __('This can be selected when replying to a ticket');?></div> + <div class="faded"><?php echo __('This can be selected when replying to a thread');?></div> </td> <td> <select name="default_signature_type"> @@ -250,6 +300,23 @@ if ($avatar->isChangeable()) { ?> <div class="error"><?php echo $errors['timezone']; ?></div> </td> </tr> + <tr><td><?php echo __('Time Format');?>:</td> + <td> + <select name="datetime_format"> +<?php + $datetime_format = $staff->datetime_format; + foreach (array( + 'relative' => __('Relative Time'), + '' => '— '.__('System Default').' —', +) as $v=>$name) { ?> + <option value="<?php echo $v; ?>" <?php + if ($v == $datetime_format) + echo 'selected="selected"'; + ?>><?php echo $name; ?></option> +<?php } ?> + </select> + </td> + </tr> <?php if ($cfg->getSecondaryLanguages()) { ?> <tr> <td><?php echo __('Preferred Language'); ?>:</td> diff --git a/include/staff/settings-agents.inc.php b/include/staff/settings-agents.inc.php index e5ca64b320ce14ec1f601dd4db4e55583092d96c..2a06676f0bc6d82355ebaa84a78ea42067933085 100644 --- a/include/staff/settings-agents.inc.php +++ b/include/staff/settings-agents.inc.php @@ -35,6 +35,14 @@ if (!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$config <i class="help-tip icon-question-sign" href="#agent_name_format"></i> </td> </tr> + <tr> + <td><?php echo __('Agent Identity Masking'); ?>:</td> + <td> + <input type="checkbox" name="hide_staff_name" <?php echo $config['hide_staff_name']?'checked="checked"':''; ?>> + <?php echo __("Hide agent's name on responses."); ?> + <i class="help-tip icon-question-sign" href="#staff_identity_masking"></i> + </td> + </tr> <tr> <td width="180"><?php echo __('Avatar Source'); ?>:</td> <td> diff --git a/include/staff/settings-tickets.inc.php b/include/staff/settings-tickets.inc.php index 4fe241476415916aac3d6295ee943fd12ef6caf9..751df274c22d7a4264bd83eda61c2e6111a8664a 100644 --- a/include/staff/settings-tickets.inc.php +++ b/include/staff/settings-tickets.inc.php @@ -203,14 +203,6 @@ if(!($maxfileuploads=ini_get('max_file_uploads'))) <i class="help-tip icon-question-sign" href="#answered_tickets"></i> </td> </tr> - <tr> - <td><?php echo __('Agent Identity Masking'); ?>:</td> - <td> - <input type="checkbox" name="hide_staff_name" <?php echo $config['hide_staff_name']?'checked="checked"':''; ?>> - <?php echo __("Hide agent's name on responses."); ?> - <i class="help-tip icon-question-sign" href="#staff_identity_masking"></i> - </td> - </tr> <tr> <th colspan="2"> <em><b><?php echo __('Attachments');?></b>: <?php echo __('Size and maximum uploads setting mainly apply to web tickets.');?></em> diff --git a/include/staff/templates/task-view.tmpl.php b/include/staff/templates/task-view.tmpl.php index 47acc7a7cab3cb527026fcd8705953e737f42a01..a2a95ff4ebd01632f320c40a97fa89d4fbfac89e 100644 --- a/include/staff/templates/task-view.tmpl.php +++ b/include/staff/templates/task-view.tmpl.php @@ -311,7 +311,8 @@ if (!$ticket) { ?> $task->getThread()->render(array('M', 'R', 'N'), array( 'mode' => Thread::MODE_STAFF, - 'container' => 'taskThread' + 'container' => 'taskThread', + 'sort' => $thisstaff->thread_view_order ) ); ?> diff --git a/include/staff/templates/thread-entries.tmpl.php b/include/staff/templates/thread-entries.tmpl.php index a3f3bdfc725a80b0c636933286c3b50b63d03e01..cabd7560b722df841904cfda1398f87bd93eb0ab 100644 --- a/include/staff/templates/thread-entries.tmpl.php +++ b/include/staff/templates/thread-entries.tmpl.php @@ -1,5 +1,15 @@ <?php -$events = $events->order_by('id'); + +$sort = 'id'; +if ($options['sort'] && !strcasecmp($options['sort'], 'DESC')) + $sort = '-id'; + +$cmp = function ($a, $b) use ($sort) { + return ($sort == 'id') + ? ($a < $b) : $a > $b; +}; + +$events = $events->order_by($sort); $events = $events->getIterator(); $events->rewind(); $event = $events->current(); @@ -33,7 +43,7 @@ foreach (Attachment::objects()->filter(array( // changes in dates between thread items. foreach ($entries as $entry) { // Emit all events prior to this entry - while ($event && $event->timestamp < $entry->created) { + while ($event && $cmp($event->timestamp, $entry->created)) { $event->render(ThreadEvent::MODE_STAFF); $events->next(); $event = $events->current(); @@ -79,6 +89,7 @@ foreach (Attachment::objects()->filter(array( $('#'+container).data('imageUrls', <?php echo JsonDataEncoder::encode($urls); ?>); // Trigger thread processing. if ($.thread) - $.thread.onLoad(container); + $.thread.onLoad(container, + {autoScroll: <?php echo $sort == 'id' ? 'true' : 'false'; ?>}); }); </script> diff --git a/include/staff/templates/thread-entry.tmpl.php b/include/staff/templates/thread-entry.tmpl.php index eeec47f8b85e2c4cfeb0c2f37fc8dfbc03e86ee8..5ae84d4eb6e716748e868f4e7c1fe44d9d68111c 100644 --- a/include/staff/templates/thread-entry.tmpl.php +++ b/include/staff/templates/thread-entry.tmpl.php @@ -1,4 +1,12 @@ <?php +global $thisstaff; +$timeFormat = null; +if ($thisstaff && !strcasecmp($thisstaff->datetime_format, 'relative')) { + $timeFormat = function($datetime) { + return Format::relativeTime(Misc::db2gmtime($datetime)); + }; +} + $entryTypes = array('M'=>'message', 'R'=>'response', 'N'=>'note'); $user = $entry->getUser() ?: $entry->getStaff(); $name = $user ? $user->getName() : $entry->poster; @@ -51,16 +59,17 @@ if ($user) </span> </div> <?php - echo sprintf(__('<b>%s</b> posted %s'), - $entry->isSystem() ? __('SYSTEM') : $name, - sprintf('<a name="entry-%d" href="#entry-%1$s"><time class="relative" datetime="%s" title="%s">%s</time></a>', + echo sprintf(__('<b>%s</b> posted %s'), $name, + sprintf('<a name="entry-%d" href="#entry-%1$s"><time %s + datetime="%s" data-toggle="tooltip" title="%s">%s</time></a>', $entry->id, + $timeFormat ? 'class="relative"' : '', date(DateTime::W3C, Misc::db2gmtime($entry->created)), Format::daydatetime($entry->created), - Format::relativeTime(Misc::db2gmtime($entry->created)) + $timeFormat ? $timeFormat($entry->created) : Format::datetime($entry->created) ) ); ?> - <span style="max-width:500px" class="faded title truncate"><?php + <span style="max-width:400px" class="faded title truncate"><?php echo $entry->title; ?></span> </span> </div> diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index 16e22b0168cb1b3cdaa7d7e4c42bee11f501186b..ca3ba69cdbdc3092cd8ded141dd72130f7015ecd 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -497,8 +497,10 @@ $tcount = $ticket->getThreadEntries($types)->count(); $ticket->getThread()->render( array('M', 'R', 'N'), array( - 'html-id' => 'ticketThread', - 'mode' => Thread::MODE_STAFF) + 'html-id' => 'ticketThread', + 'mode' => Thread::MODE_STAFF, + 'sort' => $thisstaff->thread_view_order + ) ); ?> <div class="clear"></div> diff --git a/scp/css/scp.css b/scp/css/scp.css index d487797241bd860f90d5ba51bfd9babe32304666..1aa6361639c51abee2bff81811197a7d4fb31669 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -74,7 +74,7 @@ div#header a { time[title]:hover { text-decoration: underline; } -a time.relative { +a time { color: initial; }