diff --git a/css/thread.css b/css/thread.css index 4ac6615703681721278f9d2f8815c499d2a6aff9..5d8ecb7a2014832c05decdcaf34321399cf4f68e 100644 --- a/css/thread.css +++ b/css/thread.css @@ -66,7 +66,7 @@ .thread-body kbd, .thread-body pre, .thread-body samp { - font-family: monospace, serif; + font-family: 'Source Code Pro', 'Monaco', 'Consolas', monospace, serif; font-size: 1em; } .thread-body pre { @@ -420,7 +420,10 @@ margin: 0; margin-bottom: 10px; border: none; - background: none !important; + background: #f5f5f5; + background-color: rgba(0,0,0,0.05); + border-radius: 5px; + padding: 0.5em; box-shadow: none !important; text-indent: 0 !important; } diff --git a/include/class.orm.php b/include/class.orm.php index d8e70f6007b086fe77ab20ab0a79da96c97780eb..676390409859f92f996ab167e3987bdf8a304a66 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -588,6 +588,7 @@ class AnnotatedModel { return $this->annotations[$what]; return $this->model->get($what, null); } + function __set($what, $to) { return $this->set($what, $to); } @@ -597,6 +598,10 @@ class AnnotatedModel { return $this->model->set($what, $to); } + function __isset($what) { + return isset($this->annotations[$what]) || $this->model->__isset($what); + } + // Delegate everything else to the model function __call($what, $how) { return call_user_func_array(array($this->model, $what), $how); diff --git a/include/class.thread.php b/include/class.thread.php index 2b8be9fa597ec0ecbdcef95a24088f04a8cfd2e7..00623c85eb1020c12bc70e7500a8bed0ea191c20 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -381,10 +381,10 @@ class Thread extends VerySimpleModel { // Try not to destroy the format of the body $header = sprintf("Received From: %s <%s>\n\n", $mailinfo['name'], $mailinfo['email']); - if ($body instanceof HtmlThreadBody) + if ($body instanceof HtmlThreadEntryBody) $header = nl2br(Format::htmlchars($header)); // Add the banner to the top of the message - if ($body instanceof ThreadBody) + if ($body instanceof ThreadEntryBody) $body->prepend($header); $vars['message'] = $body; $vars['userId'] = 0; //Unknown user! //XXX: Assume ticket owner? @@ -565,6 +565,7 @@ implements TemplateVariable { const FLAG_EDITED = 0x0002; const FLAG_HIDDEN = 0x0004; const FLAG_GUARDED = 0x0008; // No replace on edit + const FLAG_RESENT = 0x0010; const PERM_EDIT = 'thread.edit'; @@ -762,6 +763,17 @@ implements TemplateVariable { return $this->user; } + function getEditor() { + static $types = array( + 'U' => 'User', + 'S' => 'Staff', + ); + if (!isset($types[$this->editor_type])) + return null; + + return $types[$this->editor_type]::lookup($this->editor); + } + function getName() { if ($this->staff_id) return $this->staff->getName(); @@ -1527,6 +1539,7 @@ class ThreadEvent extends VerySimpleModel { 'edited' => 'pencil', 'closed' => 'thumbs-up-alt', 'reopened' => 'rotate-right', + 'resent' => 'reply-all icon-flip-horizontal', ); return @$icons[$this->state] ?: 'chevron-sign-right'; } @@ -1595,6 +1608,7 @@ class ThreadEvent extends VerySimpleModel { return ''; return sprintf($base, implode(', ', $changes)); }, + 'resent' => __('<b>{username}</b> resent <strong><a href="#thread-entry-{data.entry}">a previous response</a></strong> {timestamp}'), ); $self = $this; $data = $this->getData(); diff --git a/include/class.thread_actions.php b/include/class.thread_actions.php index 15758498fdabb968929300fded2c0402f3af7610..df4cf37e02e1d48cb087a47b293053d20ab17236 100644 --- a/include/class.thread_actions.php +++ b/include/class.thread_actions.php @@ -88,12 +88,12 @@ $.dialog(url, [201], function(xhr, resp) { var json = JSON.parse(resp); if (!json || !json.thread_id) return; - $('#thread-id-'+json.thread_id) - .attr('id', 'thread-id-' + json.new_id) - .find('div') - .html(json.body) - .closest('td') - .effect('highlight') + $('#thread-entry-'+json.thread_id) + .attr('id', 'thread-entry-' + json.new_id) + .html(json.entry) + .find('.thread-body') + .delay(500) + .effect('highlight'); }, {size:'large'}); JS , $this->getAjaxUrl()); @@ -118,10 +118,10 @@ JS } function updateEntry($guard=false) { + global $thisstaff; + $old = $this->entry; - $type = ($old->format == 'html') - ? 'HtmlThreadEntryBody' : 'TextThreadEntryBody'; - $new = new $type($_POST['body']); + $new = ThreadEntryBody::fromFormattedText($_POST['body'], $old->format); if ($new->getClean() == $old->body) // No update was performed @@ -139,7 +139,7 @@ JS 'pid' => $old->id, // Add in new stuff - 'title' => $_POST['title'], + 'title' => Format::htmlchars($_POST['title']), 'body' => $new, 'ip_address' => $_SERVER['REMOTE_ADDR'], )); @@ -151,6 +151,8 @@ JS // that way for email header lookups and such to remain consistent if ($old->flags & ThreadEntry::FLAG_EDITED + // If editing another person's edit, make a new entry + and ($old->editor == $thisstaff->getId() && $old->editor_type == 'S') and !($old->flags & ThreadEntry::FLAG_GUARDED) ) { // Replace previous edit -------------------------- @@ -162,20 +164,24 @@ JS $old = $original; } - // Mark the new entry as edited (but not hidden) - $entry->flags = ($old->flags & ~ThreadEntry::FLAG_HIDDEN) + // Mark the new entry as edited (but not hidden nor guarded) + $entry->flags = ($old->flags & ~(ThreadEntry::FLAG_HIDDEN | ThreadEntry::FLAG_GUARDED)) | ThreadEntry::FLAG_EDITED; // Guard against deletes on future edit if requested. This is done // if an email was triggered by the last edit. In such a case, it - // should not be replace by a subsequent edit. + // should not be replaced by a subsequent edit. if ($guard) $entry->flags |= ThreadEntry::FLAG_GUARDED; - // Sort in the same place in the thread — XXX: Add a `sequence` id + // Log the editor + $entry->editor = $thisstaff->getId(); + $entry->editor_type = 'S'; + + // Sort in the same place in the thread $entry->created = $old->created; $entry->updated = SqlFunction::NOW(); - $entry->save(); + $entry->save(true); // Hide the old entry from the object thread $old->flags |= ThreadEntry::FLAG_HIDDEN; @@ -190,10 +196,14 @@ JS if (!($entry = $this->updateEntry())) return $this->trigger__get(); + ob_start(); + include STAFFINC_DIR . 'templates/thread-entry.tmpl.php'; + $content = ob_get_clean(); + Http::response('201', JsonDataEncoder::encode(array( - 'thread_id' => $this->entry->id, + 'thread_id' => $this->entry->id, # This is the old id! 'new_id' => $entry->id, - 'body' => $entry->getBody()->toHtml(), + 'entry' => $content, ))); } } @@ -250,13 +260,17 @@ class TEA_EditAndResendThreadEntry extends TEA_EditThreadEntry { if (!($entry = $this->updateEntry($resend))) return $this->trigger__get(); - if (@$_POST['commit'] == 'resend') + if ($resend) $this->resend($entry); + ob_start(); + include STAFFINC_DIR . 'templates/thread-entry.tmpl.php'; + $content = ob_get_clean(); + Http::response('201', JsonDataEncoder::encode(array( - 'thread_id' => $this->entry->id, + 'thread_id' => $this->entry->id, # This is the old id! 'new_id' => $entry->id, - 'body' => $entry->getBody()->toHtml(), + 'entry' => $content, ))); } @@ -299,6 +313,13 @@ class TEA_EditAndResendThreadEntry extends TEA_EditThreadEntry { } // TODO: Add an option to the dialog $ticket->notifyCollaborators($response, array('signature' => $signature)); + + // Log an event that the item was resent + $ticket->logEvent('resent', array('entry' => $response->id)); + + // Flag the entry as resent + $response->flags |= ThreadEntry::FLAG_RESENT; + $response->save(); } } ThreadEntry::registerAction(/* trans */ 'Manage', 'TEA_EditAndResendThreadEntry'); diff --git a/include/staff/templates/thread-entries.tmpl.php b/include/staff/templates/thread-entries.tmpl.php index 0be5722c5004d59e9bf62415af5413f5c249e247..7ac199444b8b2607346451a97e5435cb5dda0753 100644 --- a/include/staff/templates/thread-entries.tmpl.php +++ b/include/staff/templates/thread-entries.tmpl.php @@ -28,7 +28,9 @@ if (count($entries)) { $events->next(); $event = $events->current(); } + ?><div id="thread-entry-<?php echo $entry->getId(); ?>"><?php include STAFFINC_DIR . 'templates/thread-entry.tmpl.php'; + ?></div><?php } $i++; } diff --git a/include/staff/templates/thread-entry-edit.tmpl.php b/include/staff/templates/thread-entry-edit.tmpl.php index bab5e4eb65223cb4705a8499a3909ed58a404b7b..1440780061aa0831d3009522a54f501c63425ca4 100644 --- a/include/staff/templates/thread-entry-edit.tmpl.php +++ b/include/staff/templates/thread-entry-edit.tmpl.php @@ -33,7 +33,7 @@ class="large <?php if ($cfg->isRichTextEnabled() && $this->entry->format == 'html') echo 'richtext'; - ?>"><?php echo Format::viewableImages($this->entry->body); + ?>"><?php echo htmlspecialchars(Format::viewableImages($this->entry->body)); ?></textarea> <?php if ($this->entry->type == 'R') { ?> diff --git a/include/staff/templates/thread-entry-view.tmpl.php b/include/staff/templates/thread-entry-view.tmpl.php index 51a0a10da8bc411507c049337ee7cc2b92a86e5b..0b5a542bae73f87eadfa31a810487042c05e54f5 100644 --- a/include/staff/templates/thread-entry-view.tmpl.php +++ b/include/staff/templates/thread-entry-view.tmpl.php @@ -16,7 +16,7 @@ do { // If you originally posted it, you can see all the edits && $E->staff_id != $thisstaff->getId() // You can see your own edits - // && $E->editor != $thisstaff->getId() + && ($E->editor != $thisstaff->getId() || $E->editor_type != 'S') ) { // Skip edits made by other agents continue; @@ -26,7 +26,8 @@ do { <strong><?php if ($E->title) echo Format::htmlchars($E->title).' — '; ?></strong> <em><?php if (strpos($E->updated, '0000-') === false) - echo sprintf(__('Edited on %s'), Format::datetime($E->updated)); + echo sprintf(__('Edited on %s by %s'), Format::datetime($E->updated), + ($editor = $E->getEditor()) ? $editor->getName() : ''); else echo __('Original'); ?></em> </a> diff --git a/include/staff/templates/thread-entry.tmpl.php b/include/staff/templates/thread-entry.tmpl.php index e7c31e2a512eb6eb614b9e88910dafc3a0a32107..e339e04555ebcbddc5d823f3edd6b7472956f080 100644 --- a/include/staff/templates/thread-entry.tmpl.php +++ b/include/staff/templates/thread-entry.tmpl.php @@ -38,9 +38,13 @@ if ($user && ($url = $user->get_gravatar(48))) <span style="vertical-align:middle;" class="textra"> <?php if ($entry->flags & ThreadEntry::FLAG_EDITED) { ?> <span class="label label-bare" title="<?php - echo sprintf(__('Edited on %s by %s'), Format::datetime($entry->updated), 'You'); + echo sprintf(__('Edited on %s by %s'), Format::datetime($entry->updated), + ($editor = $entry->getEditor()) ? $editor->getName() : ''); ?>"><?php echo __('Edited'); ?></span> - <?php } ?> +<?php } ?> +<?php if ($entry->flags & ThreadEntry::FLAG_RESENT) { ?> + <span class="label label-bare"><?php echo __('Resent'); ?></span> +<?php } ?> </span> </div> <?php @@ -56,11 +60,15 @@ if ($user && ($url = $user->get_gravatar(48))) echo $entry->title; ?></span> </span> </div> - <div class="thread-body" id="thread-id-<?php echo $entry->getId(); ?>"> + <div class="thread-body"> <div><?php echo $entry->getBody()->toHtml(); ?></div> <div class="clear"></div> <?php - if ($entry->has_attachments) { ?> + // The strangeness here is because .has_attachments is an annotation from + // Thread::getEntries(); however, this template may be used in other + // places such as from thread entry editing + if (isset($entry->has_attachments) ? $entry->has_attachments + : $entry->attachments->filter(array('inline'=>0))->count()) { ?> <div class="attachments"><?php foreach ($entry->attachments as $A) { if ($A->inline) @@ -83,7 +91,7 @@ if ($user && ($url = $user->get_gravatar(48))) <?php if ($urls = $entry->getAttachmentUrls()) { ?> <script type="text/javascript"> - $('#thread-id-<?php echo $entry->getId(); ?>') + $('#thread-entry-<?php echo $entry->getId(); ?>') .data('urls', <?php echo JsonDataEncoder::encode($urls); ?>) .data('id', <?php echo $entry->getId(); ?>); diff --git a/scp/css/scp.css b/scp/css/scp.css index 0181a06d0fa67ae762e1e1c779de278fb5ab1350..5bd0cf2ffb60593fc49e16a9ea5af2972df3ce6a 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -2391,6 +2391,9 @@ td.indented { padding: 0 2px 15px; margin-left: 60px; } +.thread-event a { + color: inherit; +} .type-icon { border-radius: 8px; background-color: #f4f4f4; diff --git a/scp/js/scp.js b/scp/js/scp.js index ef9fd3071ba409b70e3d42f710a589ed0fc26a28..fad761841fa6a6e82b811518ab3143398617972d 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -1071,6 +1071,7 @@ function addSearchParam(key, value) { // Periodically adjust relative times window.relativeAdjust = setInterval(function() { + // Thanks, http://stackoverflow.com/a/7641822/1025836 var prettyDate = function(time) { var date = new Date((time || "").replace(/-/g, "/").replace(/[TZ]/g, " ")), diff = (((new Date()).getTime() - date.getTime()) / 1000), @@ -1083,7 +1084,7 @@ window.relativeAdjust = setInterval(function() { || diff < 120 && __("about a minute ago") || diff < 3600 && __("%d minutes ago").replace('%d', Math.floor(diff/60)) || diff < 7200 && __("about an hour ago") - || diff < 86400 && __("%d hours ago").replace('%d', Math.floor(diff/86400)) + || diff < 86400 && __("%d hours ago").replace('%d', Math.floor(diff/3600)) ) || day_diff == 1 && __("yesterday") || day_diff < 7 && __("%d days ago").replace('%d', day_diff); diff --git a/scp/js/ticket.js b/scp/js/ticket.js index 7f7b879e7eb8a13f0723ee764dd668398ff5fdca..3be939612745b3aec9ae319af1903d885cbd918f 100644 --- a/scp/js/ticket.js +++ b/scp/js/ticket.js @@ -305,7 +305,7 @@ $.showNonLocalImage = function(div) { $.showImagesInline = function(urls, thread_id) { var selector = (thread_id == undefined) ? '.thread-body img[data-cid]' - : '.thread-body#thread-id-'+thread_id+' img[data-cid]'; + : '.thread-body#thread-entry-'+thread_id+' img[data-cid]'; $(selector).each(function(i, el) { var e = $(el), cid = e.data('cid').toLowerCase(),