diff --git a/lib/util.php b/lib/util.php index d358338519..ffa92fc69f 100644 --- a/lib/util.php +++ b/lib/util.php @@ -1501,16 +1501,18 @@ function common_enqueue_notice($notice) } /** - * Broadcast profile updates to remote subscribers. + * Legacy function to broadcast profile updates to OMB remote subscribers. + * + * XXX: This probably needs killing, but there are several bits of code + * that broadcast profile changes that need to be dealt with. AFAIK + * this function is only used for OMB. -z * * Since this may be slow with a lot of subscribers or bad remote sites, * this is run through the background queues if possible. */ function common_broadcast_profile(Profile $profile) { - $qm = QueueManager::get(); - $qm->enqueue($profile, "profile"); - return true; + Event::handle('BroadcastProfile', array($profile)); } function common_profile_url($nickname) diff --git a/plugins/Event/EventPlugin.php b/plugins/Event/EventPlugin.php index 98a7d895ed..f2396b8075 100644 --- a/plugins/Event/EventPlugin.php +++ b/plugins/Event/EventPlugin.php @@ -82,6 +82,7 @@ class EventPlugin extends MicroappPlugin case 'CancelrsvpAction': case 'ShoweventAction': case 'ShowrsvpAction': + case 'TimelistAction': include_once $dir . '/' . strtolower(mb_substr($cls, 0, -6)) . '.php'; return false; case 'EventListItem': @@ -89,6 +90,7 @@ class EventPlugin extends MicroappPlugin case 'EventForm': case 'RSVPForm': case 'CancelRSVPForm': + case 'EventTimeList': include_once $dir . '/'.strtolower($cls).'.php'; break; case 'Happening': @@ -121,6 +123,8 @@ class EventPlugin extends MicroappPlugin $m->connect('rsvp/:id', array('action' => 'showrsvp'), array('id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')); + $m->connect('main/event/updatetimes', + array('action' => 'timelist')); return true; } @@ -345,7 +349,7 @@ class EventPlugin extends MicroappPlugin function onEndShowScripts($action) { - $action->inlineScript('$(document).ready(function() { $("#event-startdate").datepicker(); $("#event-enddate").datepicker(); });'); + $action->script($this->path('event.js')); } function onEndShowStyles($action) diff --git a/plugins/Event/event.css b/plugins/Event/event.css index 8c9cbbb082..7fbb67d732 100644 --- a/plugins/Event/event.css +++ b/plugins/Event/event.css @@ -6,3 +6,11 @@ .event-title { margin-left: 0px; } #content .event .entry-title { margin-left: 0px; } #content .event .entry-content { margin-left: 0px; } +.ui-autocomplete { + max-height: 100px; + overflow-y: auto; + /* prevent horizontal scrollbar */ + overflow-x: hidden; + /* add padding to account for vertical scrollbar */ + padding-right: 20px; +} \ No newline at end of file diff --git a/plugins/Event/event.js b/plugins/Event/event.js new file mode 100644 index 0000000000..8ed25a899b --- /dev/null +++ b/plugins/Event/event.js @@ -0,0 +1,73 @@ +$(document).ready(function() { + + var today = new Date(); + + $("#event-startdate").datepicker({ + // Don't let the user set a crazy start date + minDate: today, + onClose: function(dateText, picker) { + // Don't let the user set a crazy end date + var newStartDate = new Date(dateText); + var endDate = new Date($("#event-startdate").val()); + if (endDate < newStartDate) { + $("#event-enddate").val(dateText); + } + if (dateText !== null) { + $("#event-enddate").datepicker('option', 'minDate', new Date(dateText)); + } + }, + onSelect: function() { + var startd = $("#event-startdate").val(); + var endd = $("#event-enddate").val(); + var sdate = new Date(startd); + var edate = new Date(endd); + if (sdate !== edate) { + updateTimes(); + } + } + }); + + $("#event-enddate").datepicker({ + minDate: today, + onSelect: function() { + var startd = $("#event-startdate").val(); + var endd = $("#event-enddate").val(); + var sdate = new Date(startd); + var edate = new Date(endd); + if (sdate !== edate) { + updateTimes(); + } + } + }); + + function updateTimes() { + var startd = $("#event-startdate").val(); + var endd = $("#event-enddate").val(); + + var startt = $("#event-starttime option:selected").val(); + var endt = $("#event-endtime option:selected").val(); + + var sdate = new Date(startd + " " + startt); + var edate = new Date(endd + " " + endt); + var duration = (startd === endd); + + $.getJSON($('#timelist_action_url').val(), + { start: startt, ajax: true, duration: duration }, + function(data) { + var times = []; + $.each(data, function(key, val) { + times.push(''); + }); + + $("#event-endtime").html(times.join('')); + if (startt < endt) { + $("#event-endtime").val(endt).attr("selected", "selected"); + } + }) + } + + $("#event-starttime").change(function(e) { + updateTimes(); + }); + +}); diff --git a/plugins/Event/eventform.php b/plugins/Event/eventform.php index 6a6e17e77b..d7c554bf32 100644 --- a/plugins/Event/eventform.php +++ b/plugins/Event/eventform.php @@ -84,6 +84,17 @@ class EventForm extends Form function formData() { $this->out->elementStart('fieldset', array('id' => 'new_event_data')); + + // Passing in the URL of the Ajax action that the .js for this form hits + // when selecting event start and end times. JavaScript will try to + // use a relative path, unless explicitely told where an action is, + // and that's a bit difficult to calculate since the event form is on + // so many pages with different paths. It might be worth solving this + // globally by putting the base site path in the Identifier-URL meta tag + // or something similar, so it would be easy to calculate the exact path + // for actions and other things in JavaScripts. -z + $this->out->hidden('timelist_action_url', common_local_url('timelist')); + $this->out->elementStart('ul', 'form_data'); $this->li(); @@ -97,49 +108,71 @@ class EventForm extends Form $this->unli(); $this->li(); + + $today = new DateTime('today'); + $today->setTimezone(new DateTimeZone(common_timezone())); + $this->out->input('event-startdate', // TRANS: Field label on event form. _m('LABEL','Start date'), - null, + $today->format('m/d/Y'), // TRANS: Field title on event form. _m('Date the event starts.'), 'startdate'); $this->unli(); $this->li(); - $this->out->input('event-starttime', - // TRANS: Field label on event form. - _m('LABEL','Start time'), - null, - // TRANS: Field title on event form. - _m('Time the event starts.'), - 'starttime'); + + $times = EventTimeList::getTimes(); + + $this->out->dropdown( + 'event-starttime', + // TRANS: Field label on event form. + _m('LABEL','Start time'), + $times, + // TRANS: Field title on event form. + _m('Time the event starts.'), + false, + null + ); + $this->unli(); $this->li(); $this->out->input('event-enddate', // TRANS: Field label on event form. _m('LABEL','End date'), - null, + $today->format('m/d/Y'), // TRANS: Field title on event form. _m('Date the event ends.'), 'enddate'); $this->unli(); $this->li(); - $this->out->input('event-endtime', - // TRANS: Field label on event form. - _m('LABEL','End time'), - null, - // TRANS: Field title on event form. - _m('Time the event ends.'), - 'endtime'); + + // XXX: Initial end time should be at least 30 mins out? We could do + // every 15 minute instead -z + $keys = array_keys($times); + $endStr = date('m/d/y', strtotime('now')) . " {$keys[0]}"; + $end = new DateTime($endStr); + $end->modify('+30'); + + $this->out->dropdown( + 'event-endtime', + // TRANS: Field label on event form. + _m('LABEL','End time'), + EventTimeList::getTimes($end->format('c'), true), + // TRANS: Field title on event form. + _m('Time the event ends.'), + false, + null + ); $this->unli(); $this->li(); $this->out->input('event-location', // TRANS: Field label on event form. - _m('LABEL','Location'), + _m('LABEL','Where?'), null, // TRANS: Field title on event form. _m('Event location.'), diff --git a/plugins/Event/eventlistitem.php b/plugins/Event/eventlistitem.php index 9bf34e765b..fb27704461 100644 --- a/plugins/Event/eventlistitem.php +++ b/plugins/Event/eventlistitem.php @@ -83,13 +83,33 @@ class EventListItem extends NoticeListItemAdapter $out->elementEnd('h3'); // VEVENT/H3 OUT - $startDate = strftime("%x", strtotime($event->start_time)); - $startTime = strftime("%R", strtotime($event->start_time)); + $now = new DateTime(); + $startDate = new DateTime($event->start_time); + $endDate = new DateTime($event->end_time); + $userTz = new DateTimeZone(common_timezone()); - $endDate = strftime("%x", strtotime($event->end_time)); - $endTime = strftime("%R", strtotime($event->end_time)); + // Localize the time for the observer + $now->setTimeZone($userTz); + $startDate->setTimezone($userTz); + $endDate->setTimezone($userTz); - // FIXME: better dates + $thisYear = $now->format('Y'); + $startYear = $startDate->format('Y'); + $endYear = $endDate->format('Y'); + + $dateFmt = 'D, F j, '; // e.g.: Mon, Aug 31 + + if ($startYear != $thisYear || $endYear != $thisYear) { + $dateFmt .= 'Y,'; // append year if we need to think about years + } + + $startDateStr = $startDate->format($dateFmt); + $endDateStr = $endDate->format($dateFmt); + + $timeFmt = 'g:ia'; + + $startTimeStr = $startDate->format($timeFmt); + $endTimeStr = $endDate->format("{$timeFmt} (T)"); $out->elementStart('div', 'event-times'); // VEVENT/EVENT-TIMES IN @@ -98,16 +118,16 @@ class EventListItem extends NoticeListItemAdapter $out->element('abbr', array('class' => 'dtstart', 'title' => common_date_iso8601($event->start_time)), - $startDate . ' ' . $startTime); - $out->text(' - '); - if ($startDate == $endDate) { + $startDateStr . ' ' . $startTimeStr); + $out->text(' – '); + if ($startDateStr == $endDateStr) { $out->element('span', array('class' => 'dtend', 'title' => common_date_iso8601($event->end_time)), - $endTime); + $endTimeStr); } else { $out->element('span', array('class' => 'dtend', 'title' => common_date_iso8601($event->end_time)), - $endDate . ' ' . $endTime); + $endDateStr . ' ' . $endTimeStr); } $out->elementEnd('div'); // VEVENT/EVENT-TIMES OUT diff --git a/plugins/Event/eventtimelist.php b/plugins/Event/eventtimelist.php new file mode 100644 index 0000000000..4ca40cb61f --- /dev/null +++ b/plugins/Event/eventtimelist.php @@ -0,0 +1,119 @@ + + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2011, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +/** + * Class to get fancy times for the dropdowns on the new event form + */ +class EventTimeList { + + /** + * Round up to the nearest half hour + * + * @param string $time the time to round (date/time string) + * @return DateTime the rounded time + */ + public static function nearestHalfHour($time) + { + $start = strtotime($time); + + $minutes = date('i', $start); + $hour = date('H', $start); + + if ($minutes >= 30) { + $minutes = '00'; + $hour++; + } else { + $minutes = '30'; + } + + $newTimeStr = date('m/d/y', $start) . " {$hour}:{$minutes}:00"; + return new DateTime($newTimeStr); + } + + /** + * Output a list of times in half-hour intervals + * + * @param string $start Time to start with (date/time string) + * @param boolean $duration Whether to include the duration of the event + * (from the start) + * @return array $times (UTC time string => localized time string) + */ + public static function getTimes($start = 'now', $duration = false) + { + $newTime = self::nearestHalfHour($start); + + $newTime->setTimezone(new DateTimeZone(common_timezone())); + $times = array(); + $len = 0; + + for ($i = 0; $i < 48; $i++) { + + // make sure we store the time as UTC + $newTime->setTimezone(new DateTimeZone('UTC')); + $utcTime = $newTime->format('H:i:s'); + + // localize time for user + $newTime->setTimezone(new DateTimeZone(common_timezone())); + $localTime = $newTime->format('g:ia'); + + // pretty up the end-time option list a bit + if ($duration) { + $len += 30; + $hours = $len / 60; + // for i18n + $hourStr = _m('hour'); + $hoursStr = _m('hrs'); + $minStr = _m('mins'); + switch ($hours) { + case 0: + $total = " (0 {$minStr})"; + break; + case .5: + $total = " (30 {$minStr})"; + break; + case 1: + $total = " (1 {$hourStr})"; + break; + default: + $total = " ({$hours} " . $hoursStr . ')'; + break; + } + $localTime .= $total; + } + + $times[$utcTime] = $localTime; + $newTime->modify('+30min'); // 30 min intervals + } + + return $times; + } + +} + + diff --git a/plugins/Event/newevent.php b/plugins/Event/newevent.php index cadf0e1433..2704501abd 100644 --- a/plugins/Event/newevent.php +++ b/plugins/Event/newevent.php @@ -52,8 +52,8 @@ class NeweventAction extends Action protected $title = null; protected $location = null; protected $description = null; - protected $startTime = null; - protected $endTime = null; + protected $startTime = null; + protected $endTime = null; /** * Returns the title of the action @@ -89,67 +89,78 @@ class NeweventAction extends Action $this->checkSessionToken(); } - $this->title = $this->trimmed('title'); + try { - if (empty($this->title)) { - // TRANS: Client exception thrown when trying to post an event without providing a title. - throw new ClientException(_m('Title required.')); - } + $this->title = $this->trimmed('title'); - $this->location = $this->trimmed('location'); - $this->url = $this->trimmed('url'); - $this->description = $this->trimmed('description'); + if (empty($this->title)) { + // TRANS: Client exception thrown when trying to post an event without providing a title. + throw new ClientException(_m('Title required.')); + } - $startDate = $this->trimmed('startdate'); + $this->location = $this->trimmed('location'); + $this->url = $this->trimmed('url'); + $this->description = $this->trimmed('description'); - if (empty($startDate)) { - // TRANS: Client exception thrown when trying to post an event without providing a start date. - throw new ClientException(_m('Start date required.')); - } + $startDate = $this->trimmed('startdate'); - $startTime = $this->trimmed('starttime'); + if (empty($startDate)) { + // TRANS: Client exception thrown when trying to post an event without providing a start date. + throw new ClientException(_m('Start date required.')); + } - if (empty($startTime)) { - $startTime = '00:00'; - } + $startTime = $this->trimmed('event-starttime'); - $endDate = $this->trimmed('enddate'); + if (empty($startTime)) { + $startTime = '00:00'; + } - if (empty($endDate)) { - // TRANS: Client exception thrown when trying to post an event without providing an end date. - throw new ClientException(_m('End date required.')); - } + $endDate = $this->trimmed('enddate'); - $endTime = $this->trimmed('endtime'); + if (empty($endDate)) { + // TRANS: Client exception thrown when trying to post an event without providing an end date. + throw new ClientException(_m('End date required.')); + } - if (empty($endTime)) { - $endTime = '00:00'; - } + $endTime = $this->trimmed('event-endtime'); - $start = $startDate . ' ' . $startTime; + if (empty($endTime)) { + $endTime = '00:00'; + } - common_debug("Event start: '$start'"); + $start = $startDate . ' ' . $startTime; - $end = $endDate . ' ' . $endTime; + common_debug("Event start: '$start'"); - common_debug("Event start: '$end'"); + $end = $endDate . ' ' . $endTime; - $this->startTime = strtotime($start); - $this->endTime = strtotime($end); + common_debug("Event start: '$end'"); - if ($this->startTime == 0) { - // TRANS: Client exception thrown when trying to post an event with a date that cannot be processed. - // TRANS: %s is the data that could not be processed. - throw new Exception(sprintf(_m('Could not parse date "%s".'), - $start)); - } + $this->startTime = strtotime($start); + $this->endTime = strtotime($end); + if ($this->startTime == 0) { + // TRANS: Client exception thrown when trying to post an event with a date that cannot be processed. + // TRANS: %s is the data that could not be processed. + throw new ClientException(sprintf(_m('Could not parse date "%s".'), + $start)); + } - if ($this->endTime == 0) { - // TRANS: Client exception thrown when trying to post an event with a date that cannot be processed. - // TRANS: %s is the data that could not be processed. - throw new Exception(sprintf(_m('Could not parse date "%s".'), - $end)); + if ($this->endTime == 0) { + // TRANS: Client exception thrown when trying to post an event with a date that cannot be processed. + // TRANS: %s is the data that could not be processed. + throw new ClientException(sprintf(_m('Could not parse date "%s".'), + $end)); + } + } catch (ClientException $ce) { + if ($this->boolean('ajax')) { + $this->outputAjaxError($ce->getMessage()); + return false; + } else { + $this->error = $ce->getMessage(); + $this->showPage(); + return false; + } } return true; @@ -220,9 +231,13 @@ class NeweventAction extends Action RSVP::saveNew($profile, $event, RSVP::POSITIVE); } catch (ClientException $ce) { - $this->error = $ce->getMessage(); - $this->showPage(); - return; + if ($this->boolean('ajax')) { + $this->outputAjaxError($ce->getMessage()); + } else { + $this->error = $ce->getMessage(); + $this->showPage(); + return; + } } if ($this->boolean('ajax')) { @@ -242,6 +257,23 @@ class NeweventAction extends Action } } + // @todo factor this out into a base class + function outputAjaxError($msg) + { + header('Content-Type: text/xml;charset=utf-8'); + $this->xw->startDocument('1.0', 'UTF-8'); + $this->elementStart('html'); + $this->elementStart('head'); + // TRANS: Page title after an AJAX error occurs + $this->element('title', null, _('Ajax Error')); + $this->elementEnd('head'); + $this->elementStart('body'); + $this->element('p', array('id' => 'error'), $msg); + $this->elementEnd('body'); + $this->elementEnd('html'); + return; + } + /** * Show the event form * diff --git a/plugins/Event/timelist.php b/plugins/Event/timelist.php new file mode 100644 index 0000000000..a6e0174180 --- /dev/null +++ b/plugins/Event/timelist.php @@ -0,0 +1,106 @@ +. + * + * @category Event + * @package StatusNet + * @author Zach Copley + * @copyright 2011 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +/** + * Callback handler to populate end time dropdown + */ +class TimelistAction extends Action { + + private $start; + private $duration; + + /** + * Get ready + * + * @param array $args misc. arguments + * + * @return boolean true + */ + function prepare($args) { + parent::prepare($args); + $this->start = $this->arg('start'); + $this->duration = $this->boolean('duration', false); + return true; + } + + /** + * Handle input and ouput something + * + * @param array $args $_REQUEST arguments + * + * @return void + */ + function handle($args) + { + parent::handle($args); + + if (!common_logged_in()) { + // TRANS: Error message displayed when trying to perform an action that requires a logged in user. + $this->clientError(_('Not logged in.')); + return; + } + + if (!empty($this->start)) { + $times = EventTimeList::getTimes($this->start, $this->duration); + } else { + $this->clientError(_m('Unexpected form submission.')); + return; + } + + if ($this->boolean('ajax')) { + header('Content-Type: application/json; charset=utf-8'); + print json_encode($times); + } else { + $this->clientError(_m('This action is AJAX only.')); + } + } + + /** + * Override the regular error handler to show something more + * ajaxy + * + * @param string $msg error message + * @param int $code error code + */ + function clientError($msg, $code = 400) { + if ($this->boolean('ajax')) { + header('Content-Type: application/json; charset=utf-8'); + print json_encode( + array( + 'success' => false, + 'code' => $code, + 'message' => $msg + ) + ); + } else { + parent::clientError($msg, $code); + } + } +} diff --git a/plugins/OMB/OMBPlugin.php b/plugins/OMB/OMBPlugin.php index f5fed60079..38494c8134 100644 --- a/plugins/OMB/OMBPlugin.php +++ b/plugins/OMB/OMBPlugin.php @@ -369,6 +369,18 @@ class OMBPlugin extends Plugin return true; } + /** + * Broadcast a profile over OMB + * + * @param Profile $profile to broadcast + * @return false + */ + function onBroadcastProfile($profile) { + $qm = QueueManager::get(); + $qm->enqueue($profile, "profile"); + return true; + } + /** * Plugin version info * diff --git a/theme/neo/css/display.css b/theme/neo/css/display.css index d8bb5fc693..d7a4914a7d 100644 --- a/theme/neo/css/display.css +++ b/theme/neo/css/display.css @@ -1171,9 +1171,19 @@ td.entity_profile { width: auto; } -#event-startdate, #event-starttime, #event-enddate, #event-endtime { - width: 120px; +label[for=event-starttime], label[for=event-endtime] { + display: none; +} + +#event-starttime, #event-endtime { + margin-top: -1px; + margin-bottom: -1px; + height: 2em; +} + +#event-startdate, #event-enddate { margin-right: 20px; + width: 120px; } /* Limited-scope specific styles */