diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index a5fdd3d4..f2ee6010 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -449,9 +449,9 @@ class calendar extends rcube_plugin $event['uid'] = $this->generate_uid(); // set current user as organizer - if (!$event['attendees']) { + if (FALSE && !$event['attendees']) { $identity = $this->rc->user->get_identity(); - $event['attendees'][] = array('role' => 'OWNER', 'name' => $identity['name'], 'email' => $identity['email']); + $event['attendees'][] = array('role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email']); } $this->prepare_event($event); @@ -1163,6 +1163,9 @@ class calendar extends rcube_plugin } } + // let this information be cached for 15min + send_future_expire_header(90); + echo $status; exit; } diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js index 867516dc..7ee3d250 100644 --- a/plugins/calendar/calendar_ui.js +++ b/plugins/calendar/calendar_ui.js @@ -440,7 +440,7 @@ function rcube_calendar_ui(settings) attendees_list = $('#edit-attendees-table > tbody').html(''); if (calendar.attendees && event.attendees) { for (var j=0; j < event.attendees.length; j++) - add_attendee(event.attendees[j]); + add_attendee(event.attendees[j], true); } // attachments @@ -509,6 +509,12 @@ function rcube_calendar_ui(settings) if (i.match(/^rcmfile([0-9a-z]+)/)) attachments.push(RegExp.$1); data.attachments = attachments; + + // read attendee roles + $('select.edit-attendee-role').each(function(i, elem){ + if (data.attendees[i]) + data.attendees[i].role = $(elem).val(); + }); // gather recurrence settings var freq; @@ -597,6 +603,19 @@ function rcube_calendar_ui(settings) title.select(); }; + // update event properties and attendees availability if event times have changed + var event_times_changed = function() + { + alert('event_times_changed') + if (me.selected_event) { + var allday = $('#edit-allday').get(0); + me.selected_event.start = parse_datetime(allday.checked ? '00:00' : $('#edit-starttime').val(), $('#edit-startdate').val()); + me.selected_event.end = parse_datetime(allday.checked ? '23:59' : $('#edit-endtime').val(), $('#edit-enddate').val()); + if (me.selected_event.attendees) + update_freebusy_status(me.selected_event); + } + }; + // add the given list of participants var add_attendees = function(names) { @@ -623,7 +642,7 @@ function rcube_calendar_ui(settings) } if (email) { - add_attendee({ email:email, name:name, role:'REQUIRED', status:'unknown' }); + add_attendee({ email:email, name:name, role:'REQ-PARTICIPANT', status:'NEEDS-ACTION' }); success = true; } else { @@ -635,33 +654,93 @@ function rcube_calendar_ui(settings) }; // add the given attendee to the list - var add_attendee = function(data) + var add_attendee = function(data, edit) { - var dispname = (data.email && data.name) ? data.name + ' <' + data.email + '>' : (data.email || data.name); + // check for dupes... + var exists = false; + $.each(event_attendees, function(i, v){ exists |= (v.email == data.email); }); + if (exists) + return false; + var dispname = Q(data.name || data.email); + if (data.email) + dispname = '' + dispname + ''; + + // role selection + var opts = { + 'ORGANIZER': rcmail.gettext('calendar.roleorganizer'), + 'REQ-PARTICIPANT': rcmail.gettext('calendar.rolerequired'), + 'OPT-PARTICIPANT': rcmail.gettext('calendar.roleoptional'), + 'CHAIR': rcmail.gettext('calendar.roleresource') + }; + var select = ''; + + // availability + var avail = data.email ? 'loading' : 'unknown'; + if (edit && data.role == 'ORGANIZER' && data.status == 'ACCEPTED') + avail = 'free'; + // delete icon var icon = rcmail.env.deleteicon ? '' : rcmail.gettext('delete'); var dellink = '' + icon + ''; - var html = '' + - '' + Q(dispname) + '' + - '' + '' + '' + - '' + Q(data.status) + '' + - '' + (data.role != 'OWNER' ? dellink : '') + ''; + var html = '' + select + '' + + '' + dispname + '' + + '' + + '' + Q(data.status) + '' + + '' + dellink + ''; - $('') + var tr = $('') .addClass(String(data.role).toLowerCase()) .html(html) - .appendTo(attendees_list) - .find('a.deletelink').click({ id:(data.email || data.name) }, function(e) { remove_attendee(this, e.data.id); return false; }); + .appendTo(attendees_list); + + tr.find('a.deletelink').click({ id:(data.email || data.name) }, function(e) { remove_attendee(this, e.data.id); return false; }); + + // check free-busy status + if (avail == 'loading') { + check_freebusy_status(tr.find('img.availabilityicon'), data.email, me.selected_event); + } event_attendees.push(data); }; + // iterate over all attendees and update their free-busy status display + var update_freebusy_status = function(event) + { + var icons = attendees_list.find('img.availabilityicon'); + for (var i=0; i < event.attendees.length; i++) { + if (icons.get(i) && event.attendees[i].email && event.attendees[i].status != 'ACCEPTED') + check_freebusy_status(icons.get(i), event.attendees[i].email, event); + } + }; + + // load free-busy status from server and update icon accordingly + var check_freebusy_status = function(icon, email, event) + { + icon = $(icon).removeClass().addClass('availabilityicon loading'); + + $.ajax({ + type: 'GET', + dataType: 'html', + url: rcmail.url('freebusy-status'), + data: { email:email, start:date2unixtime(event.start), end:date2unixtime(event.end), _remote: 1 }, + success: function(status){ + icon.removeClass('loading').addClass(String(status).toLowerCase()); + }, + error: function(){ + icon.removeClass('loading').addClass('unknown'); + } + }); + }; + // remove an attendee from the list var remove_attendee = function(elem, id) { - $(elem).closest('tr').hide(); + $(elem).closest('tr').remove(); event_attendees = $.grep(event_attendees, function(data){ return (data.name != id && data.email != id) }); }; @@ -1234,7 +1313,8 @@ function rcube_calendar_ui(settings) } }); $('#edit-enddate, input.edit-alarm-date').datepicker(datepicker_settings); - $('#edit-startdate').datepicker(datepicker_settings).datepicker('option', 'onSelect', shift_enddate).change(function(){ shift_enddate(this.value); }); + $('#edit-startdate').datepicker(datepicker_settings).datepicker('option', 'onSelect', shift_enddate).change(function(){ shift_enddate(this.value); event_times_changed(); }); + $('#edit-enddate, #edit-starttime, #edit-endtime').change(function(){ event_times_changed(); }); $('#edit-allday').click(function(){ $('#edit-starttime, #edit-endtime')[(this.checked?'hide':'show')](); }); // configure drop-down menu on time input fields based on jquery UI autocomplete diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php index 01c68d52..be9bde5e 100644 --- a/plugins/calendar/drivers/calendar_driver.php +++ b/plugins/calendar/drivers/calendar_driver.php @@ -282,6 +282,7 @@ abstract class calendar_driver */ public function get_freebusy_list($email, $start, $end) { + sleep(2); return false; } diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php index d7ebd10a..edc12b12 100644 --- a/plugins/calendar/drivers/kolab/kolab_calendar.php +++ b/plugins/calendar/drivers/kolab/kolab_calendar.php @@ -35,6 +35,8 @@ class kolab_calendar private $search_fields = array('title', 'description', 'location'); private $sensitivity_map = array('public', 'private', 'confidential'); private $priority_map = array('low', 'normal', 'high'); + private $role_map = array('REQ-PARTICIPANT' => 'required', 'OPT-PARTICIPANT' => 'optional', 'CHAIR' => 'resource'); + private $status_map = array('NEEDS-ACTION' => 'none', 'TENTATIVE' => 'tentative', 'CONFIRMED' => 'accepted', 'DECLINED' => 'declined'); private $month_map = array('', 'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'); private $weekday_map = array('MO'=>'monday', 'TU'=>'tuesday', 'WE'=>'wednesday', 'TH'=>'thursday', 'FR'=>'friday', 'SA'=>'saturday', 'SU'=>'sunday'); @@ -430,6 +432,8 @@ class kolab_calendar $sensitivity_map = array_flip($this->sensitivity_map); $priority_map = array_flip($this->priority_map); + $status_map = array_flip($this->status_map); + $role_map = array_flip($this->role_map); if (!empty($rec['_attachments'])) { foreach ($rec['_attachments'] as $name => $attachment) { @@ -444,22 +448,22 @@ class kolab_calendar if ($rec['organizer']) { $attendees[] = array( - 'role' => 'OWNER', + 'role' => 'ORGANIZER', 'name' => $rec['organizer']['display-name'], 'email' => $rec['organizer']['smtp-address'], - 'status' => 'accepted', + 'status' => 'ACCEPTED', ); } foreach ((array)$rec['attendee'] as $attendee) { $attendees[] = array( - 'role' => strtoupper($attendee['role']), + 'role' => $role_map[$attendee['role']], 'name' => $attendee['display-name'], 'email' => $attendee['smtp-address'], - 'status' => $attendee['status'], + 'status' => $status_map[$attendee['status']], ); } - + return array( 'id' => $rec['uid'], 'uid' => $rec['uid'], @@ -624,7 +628,7 @@ class kolab_calendar // process event attendees foreach ((array)$event['attendees'] as $attendee) { $role = $attendee['role']; - if ($role == 'OWNER') { + if ($role == 'ORGANIZER') { $object['organizer'] = array( 'display-name' => $attendee['name'], 'smtp-address' => $attendee['email'], @@ -634,8 +638,8 @@ class kolab_calendar $object['attendee'][] = array( 'display-name' => $attendee['name'], 'smtp-address' => $attendee['email'], - 'status' => $attendee['status'], - 'role' => strtolower($role), + 'status' => $this->status_map[$attendee['status']], + 'role' => $this->role_map[$role], ); } } diff --git a/plugins/calendar/lib/js/fullcalendar.js b/plugins/calendar/lib/js/fullcalendar.js index 36813fa3..e55c47f1 100644 --- a/plugins/calendar/lib/js/fullcalendar.js +++ b/plugins/calendar/lib/js/fullcalendar.js @@ -1539,8 +1539,8 @@ function formatDates(date1, date2, format, options) { for (i2=i+1; i2 1) { + segHash = opt('listTexts', 'future'); } } else if (segmode == 'month') { segHash = formatDate(segDate, 'MMMM yyyy'); @@ -5447,7 +5449,7 @@ function ListEventRenderer() { if (event.start < seg.start) { datestr = opt('listTexts', 'until') + ' ' + formatDate(event.end, (event.allDay || event.end.getDate() != seg.start.getDate()) ? dateFormat : timeFormat); } else if (duration > DAY_MS) { - datestr = formatDates(event.start, event.end, dateFormat + '[ - ' + dateFormat + ']'); + datestr = formatDates(event.start, event.end, dateFormat + '{ - ' + dateFormat + '}'); } else if (seg.daydiff == 0) { datestr = opt('listTexts', 'today'); } else if (seg.daydiff == 1) { @@ -5458,7 +5460,7 @@ function ListEventRenderer() { datestr = formatDate(event.start, dateFormat); } } else if (segmode != 'day') { - datestr = formatDates(event.start, event.end, dateFormat + (duration > DAY_MS ? '[ - ' + dateFormat + ']' : '')); + datestr = formatDates(event.start, event.end, dateFormat + (duration > DAY_MS ? '{ - ' + dateFormat + '}' : '')); } if (!datestr && event.allDay) { diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc index 43183ee3..a74d76db 100644 --- a/plugins/calendar/localization/en_US.inc +++ b/plugins/calendar/localization/en_US.inc @@ -90,8 +90,12 @@ $labels['alarmtitle'] = 'Upcoming events'; $labels['attendee'] = 'Participant'; $labels['role'] = 'Role'; $labels['availability'] = 'Avail.'; -$labels['confirmstate'] = 'Confirmed'; +$labels['confirmstate'] = 'Status'; $labels['addattendee'] = 'Add participant'; +$labels['roleorganizer'] = 'Organizer'; +$labels['rolerequired'] = 'Required'; +$labels['roleoptional'] = 'Optional'; +$labels['roleresource'] = 'Resource'; // event dialog tabs $labels['tabsummary'] = 'Summary'; diff --git a/plugins/calendar/skins/default/calendar.css b/plugins/calendar/skins/default/calendar.css index d9aebb71..b155b790 100644 --- a/plugins/calendar/skins/default/calendar.css +++ b/plugins/calendar/skins/default/calendar.css @@ -106,7 +106,7 @@ pre { #calendarslist li span { cursor: default; - background: url(images/calendars.png) 0 -3px no-repeat; + background: url('images/calendars.png') 0 -3px no-repeat; padding-left: 18px; } @@ -480,11 +480,10 @@ td.topalign { border-bottom: 1px solid #ddd; } -#edit-attendees-table tr.owner td { - color: #999; +#edit-attendees-table td.role { + width: 8em; } -#edit-attendees-table td.role, #edit-attendees-table td.availability { width: 4em; } @@ -507,13 +506,70 @@ td.topalign { } #edit-attendees-table thead td { - background: url(images/listheader.gif) top left repeat-x #CCC; + background: url('images/listheader.gif') top left repeat-x #CCC; } #edit-attendees-form { margin-top: 1em; } +#edit-attendees-table select.edit-attendee-role { + border: 0; + padding: 2px; + background: white; +} + +#edit-attendees-table img.availabilityicon { + width: 16px; + height: 16px; + border-radius: 4px; + -moz-border-radius: 4px; +} + +#edit-attendees-table img.availabilityicon.loading { + background: url('images/loading-small.gif') top left no-repeat; +} + +#edit-attendees-table img.availabilityicon.unknown { + background: #ccc; +} + +#edit-attendees-table img.availabilityicon.free { + background: #0c0; +} + +#edit-attendees-table img.availabilityicon.busy { + background: #c00; +} + +#edit-attendees-table tbody td.confirmstate { + overflow: hidden; + white-space: nowrap; + font-size: 75%; +/* text-indent: -2000%; */ +} + +#edit-attendees-table td.confirmstate span { + display: block; + width: 20px; +} + +#edit-attendees-table td.confirmstate span.needs-action { + +} + +#edit-attendees-table td.confirmstate span.tentative { + +} + +#edit-attendees-table td.confirmstate span.declined { + +} + +#edit-attendees-table td.confirmstate span.accepted { + +} + span.edit-alarm-set { white-space: nowrap; } diff --git a/plugins/calendar/skins/default/images/loading-small.gif b/plugins/calendar/skins/default/images/loading-small.gif new file mode 100644 index 00000000..d42f72c7 Binary files /dev/null and b/plugins/calendar/skins/default/images/loading-small.gif differ