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 */