egroupware/home/inc/class.home_ui.inc.php

826 lines
23 KiB
PHP
Raw Normal View History

<?php
/**
* EGroupware - Home - user interface
*
* @link www.egroupware.org
* @author Nathan Gray
* @copyright (c) 2013 by Nathan Gray
* @package home
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @version $Id$
*/
2016-04-28 20:06:46 +02:00
use EGroupware\Api;
use EGroupware\Api\Framework;
use EGroupware\Api\Etemplate;
/**
* General user interface object of the Home app
*
* For the purposes of the Home application, a Portlet is [part of] an application that provides
* a specific piece of content to be included as part of the Home page.
* See /home/js/Portlet.js.
*
* While Home is not WSRP complient, it does use many of the ideas, and may someday be,
* if someone wants to fully implement it.
* @link http://docs.oasis-open.org/wsrp/v2/wsrp-2.0-spec-os-01.html
*/
class home_ui
{
public $public_functions = array(
'index' => true
);
/**
* Main UI - generates the container, and aggregates all
* the portlets from the applications
*/
public function index()
{
self::setup_default_home();
// CSS for Gridster grid layout
2016-06-06 17:38:20 +02:00
Framework::includeCSS('/vendor/npm-asset/gridster/dist/jquery.gridster.css');
2016-04-28 20:06:46 +02:00
$template = new Etemplate('home.index');
2014-11-06 22:40:03 +01:00
// Get a list of portlets
$content = array(
'portlets' => $this->get_user_portlets($template)
);
2014-11-06 22:40:03 +01:00
$template->setElementAttribute('home.index','actions',$this->get_actions());
$GLOBALS['egw_info']['flags']['app_header'] = lang('home');
$GLOBALS['egw_info']['flags']['currentapp'] = 'home';
2014-11-06 22:40:03 +01:00
2018-12-20 16:06:12 +01:00
// home screen message
2016-04-28 20:06:46 +02:00
Api\Translation::add_app('mainscreen');
$greeting = Api\Translation::translate('mainscreen_message',false,'');
if($greeting == 'mainscreen_message'|| empty($greeting))
{
2016-04-28 20:06:46 +02:00
Api\Translation::add_app('mainscreen','en'); // trying the en one
$greeting = Api\Translation::translate('mainscreen_message',false,'');
}
if(!($greeting == 'mainscreen_message'|| empty($greeting)))
{
$content['mainscreen_message'] = $greeting;
}
$template->exec('home.home_ui.index', $content);
// Now run the portlets themselves
2016-04-28 20:06:46 +02:00
$attrs = array();
foreach($content['portlets'] as $portlet => $p_data)
{
$id = $p_data['id'];
if(!$id) continue;
$portlet = $this->get_portlet($id, $p_data, $content, $attrs, true);
}
}
/**
* Get a list of actions on the whole home page. Each portlet also has
* its own actions
*
* @return array of actions
*/
protected function get_actions()
{
$portlets = $this->get_portlet_list();
$add_portlets = $portlets;
$change_for_add = function(&$add_portlets) use (&$change_for_add)
{
foreach($add_portlets as $id => &$add)
{
if(is_array($add['children']))
{
$change_for_add($add['children']);
}
if($id && !$add['children'])
{
$add['id'] = 'add_' . $id;
$add['class'] = 'add_'.$id;
}
}
};
$change_for_add($add_portlets);
$actions = array(
'add' => array(
'type' => 'popup',
'caption' => 'Add',
'onExecute' => 'javaScript:app.home.add',
'children' => $add_portlets
),
// Favorites are sortable which needs special handling
);
// Add all known portlets as drop actions too. If there are multiple matches, there will be a menu
$drop_execute = 'javaScript:app.home.add_from_drop';
2014-11-06 22:40:03 +01:00
foreach($portlets as $app => &$children)
{
// Home portlets - uses link system, so all apps that support that are accepted
if(!$children['children'])
{
2014-11-06 22:40:03 +01:00
$children['class'] = $app;
$children['onExecute'] = $drop_execute;
$children['type'] = 'drop';
$actions["drop_$app"] = $children;
}
else
{
foreach($children['children'] as $portlet => $app_portlet)
{
if(!is_array($app_portlet)) continue;
$app_portlet['class'] = $portlet;
$app_portlet['id'] = 'drop_' . $app_portlet['id'];
$app_portlet['onExecute'] = $drop_execute;
$app_portlet['acceptedTypes'] = $app;
$app_portlet['type'] = 'drop';
$actions["drop_$portlet"] = $app_portlet;
}
}
}
// For admins, add the ability to set current home as a default
self::create_default_actions($actions);
return $actions;
}
/**
* Get a list of the user's portlets, and their associated values & settings, for display
*
* Actual portlet content is provided by each portlet.
* @param template etemplate so attributes can be set
*/
2016-04-28 20:06:46 +02:00
protected function get_user_portlets(Etemplate &$template)
{
2014-11-06 22:40:03 +01:00
$portlets = array();
foreach((array)$GLOBALS['egw_info']['user']['preferences']['home']as $id => $context)
{
if(strpos($id,'portlet_') !== 0 || // Not a portlet
in_array($id, array_keys($GLOBALS['egw_info']['user']['apps'])) || // Some other app put it's pref in here
!is_array($context) // Not a valid portlet (probably user deleted a default)
) continue;
2014-11-06 22:40:03 +01:00
$classname = $context['class'];
// Avoid portlets for apps user can't use (eg. from defaults/forced)
list($app,$other) = explode('_',$classname);
if(!$GLOBALS['egw_info']['apps'][$app]) $app .='_'.$other;
if(!$GLOBALS['egw_info']['user']['apps'][$app]) {
continue;
}
2014-11-06 22:40:03 +01:00
$portlet = new $classname($context);
$desc = $portlet->get_description();
$portlet_content = array(
'id' => $id,
'type' => $portlet->get_type()
) + $desc + $context;
2014-11-06 22:40:03 +01:00
// Get settings
// Exclude common attributes changed through UI and settings lacking a type
$settings = $portlet->get_properties();
foreach($settings as $key => $setting)
{
2023-03-10 22:44:01 +01:00
if(is_array($setting) && !array_key_exists('type', $setting))
{
unset($settings[$key]);
}
2014-11-06 22:40:03 +01:00
}
$settings += $context;
foreach(home_portlet::$common_attributes as $attr)
{
unset($settings[$attr]);
}
$portlet_content['settings'] = $settings;
2023-03-10 22:44:01 +01:00
if(method_exists($portlet, "get_value"))
{
$portlet_content['value'] = $portlet->get_value();
}
2014-11-06 22:40:03 +01:00
// Set actions
// Must be after settings so actions can take settings into account
$actions = $portlet->get_actions();
// Add in default for admins
self::create_default_actions($actions, $id);
$template->setElementAttribute("portlets[" . count($portlets) . "[$id]", 'actions', $actions);
2014-11-06 22:40:03 +01:00
$portlets[] = $portlet_content;
}
// Add in legacy HTML home bits
2014-11-06 22:40:03 +01:00
// TODO: DOM IDs still collide
2023-03-10 17:13:17 +01:00
$this->get_legacy_portlets($portlets, $attributes);
2014-11-06 22:40:03 +01:00
return $portlets;
}
/**
* Load the needed info for one portlet, given the context
*
* @param context Array Settings to customize the portlet instance (size, entry, etc)
* These are specific values for the portlet's properties.
* @param content String HTML fragment to be displayed - will be set by the portlet
* @param attributes Array Settings that can be customized on a per-portlet basis - will be set
2014-11-06 22:40:03 +01:00
* @param full_exec Boolean If set, the portlet etemplates should use mode 2, if not use mode -1
* @return home_portlet The portlet object that created the content
*/
2014-11-06 22:40:03 +01:00
protected function get_portlet($id, &$context, &$content, &$attributes, $full_exec = false)
{
if(!$context['class']) $context['class'] = 'home_link_portlet';
2014-11-06 22:40:03 +01:00
// This should be set already, but just in case the execution path
// is different from normal...
2016-04-28 20:06:46 +02:00
if(Api\Json\Response::isJSONResponse())
2014-11-06 22:40:03 +01:00
{
2016-04-28 20:06:46 +02:00
$GLOBALS['egw']->framework->response = Api\Json\Response::get();
2014-11-06 22:40:03 +01:00
}
$classname = $context['class'];
2014-11-06 22:40:03 +01:00
$portlet = new $classname($context, $full_exec);
$desc = $portlet->get_description();
2014-11-06 22:40:03 +01:00
// Pre-set up etemplate so it only needs done once
2016-04-28 20:06:46 +02:00
$etemplate = new Etemplate();
// Exclude common attributes changed through UI and settings lacking a type
$settings = $portlet->get_properties();
foreach($settings as $key => $setting)
{
if(is_array($setting) && !array_key_exists('type',$setting)) unset($settings[$key]);
}
$settings += $context;
foreach(home_portlet::$common_attributes as $attr)
{
unset($settings[$attr]);
}
$attributes = array(
'title' => $desc['title'],
'color' => $settings['color'] ?: "",
'settings' => $settings,
'actions' => $portlet->get_actions(),
);
// Add in default settings
self::create_default_actions($attributes['actions'], $id);
// Set any provided common attributes (size, etc)
foreach(home_portlet::$common_attributes as $name)
{
if(array_key_exists($name, $context))
{
$attributes[$name] = $context[$name];
}
}
2014-11-06 22:40:03 +01:00
foreach($attributes as $attr => $value)
{
$etemplate->setElementAttribute($id, $attr, $value);
}
// Make sure custom javascript is loaded
$appname = $context['appname'];
if(!$appname)
{
list($app) = explode('_',$classname);
if($GLOBALS['egw_info']['apps'][$app])
{
$appname = $app;
}
}
2023-03-02 21:27:35 +01:00
//Framework::includeJS('', $classname, $appname ? $appname : 'home');
2014-11-06 22:40:03 +01:00
if($full_exec)
{
$content = $portlet->exec($id, $etemplate, $full_exec ? 2 : -1);
}
return $portlet;
}
/**
* Get a list of pre-etemplate2 home hook content according to the individual
* application preferences. If we find a preference that indicates the user
* wants some content, we make a portlet for that app using the home_legacy_portlet,
* which fetches content from the home hook.
*/
2014-11-06 22:40:03 +01:00
protected function get_legacy_portlets(&$content, &$attributes)
{
$sorted_apps = array_keys($GLOBALS['egw_info']['user']['apps']);
$portal_oldvarnames = array('mainscreen_showevents', 'homeShowEvents','homeShowLatest','mainscreen_showmail','mainscreen_showbirthdays','mainscreen_show_new_updated', 'homepage_display');
foreach($sorted_apps as $appname)
{
// If there's already [new] settings, or no preference, skip it
if($content[$appname]) continue;
$no_pref = true;
foreach($portal_oldvarnames as $varcheck)
{
$thisd = $GLOBALS['egw_info']['user']['preferences'][$appname][$varcheck];
if($thisd)
{
$no_pref = false;
break;
}
}
2016-04-28 20:06:46 +02:00
if($no_pref || !Api\Hooks::exists('home', $appname))
{
continue;
}
$context = array(
2023-03-10 17:13:17 +01:00
'class' => 'home_legacy_portlet',
'app' => $appname,
'type' => 'et2-portlet',
'width' => 8, 'height' => 3
);
$_content = '';
$_attributes = array();
$this->get_portlet($appname, $context, $_content, $_attributes);
if(trim($_content))
{
$content[$appname] = $_content;
$attributes[$appname] = $_attributes;
}
}
}
/**
* Get a list of all available portlets for add menu
*/
protected function get_portlet_list()
{
2016-04-28 20:06:46 +02:00
$list = Api\Cache::getTree('home', 'portlet_classes', function() {
$list = array();
$classes = array();
// Ignore some problem files and base classes that shouldn't be options
$ignore = array(
'.','..',
'class.home_portlet.inc.php',
'class.home_legacy_portlet.inc.php',
'class.home_favorite_portlet.inc.php'
);
// Look through all known classes for portlets - for now, they need 'portlet' in the file name
foreach(array_keys($GLOBALS['egw_info']['apps']) as $appname)
{
if(in_array($appname, array('phpgwapi', 'felamimail'))) continue;
$files = (array)@scandir(EGW_SERVER_ROOT . '/'.$appname .'/inc/');
if(!$files) continue;
foreach($files as $entry)
{
if (!in_array($entry, $ignore) && substr($entry,-8) == '.inc.php' && strpos($entry,'portlet'))
{
list(,$classname) = explode('.', $entry);
if(class_exists($classname) &&
in_array('home_portlet', class_parents($classname, false)))
{
$classes[$appname][] = $classname;
}
else
{
error_log("Could not load $classname from $entry");
}
}
}
if(!$classes[$appname]) continue;
// Build 'Add' actions for each discovered portlet.
// Portlets from other apps go in sub-actions
$add_to =& $list;
if($classes[$appname] && $appname != 'home')
{
$list[$appname] = array(
'caption' => lang($appname),
);
$add_to =& $list[$appname]['children'];
}
foreach($classes[$appname] as $portlet)
{
$instance = new $portlet();
$desc = $instance->get_description();
$add_to[$portlet] = array(
'id' => $portlet,
'caption' => $desc['displayName'],
'hint' => $desc['description'],
'onExecute' => 'javaScript:app.home.add',
'acceptedTypes' => $instance->accept_drop(),
'allowOnMultiple' => $instance->accept_multiple()
);
}
}
return $list;
}, array(), 60);
// Filter list by current user's permissions
2016-04-28 20:06:46 +02:00
foreach(array_keys($list) as $appname)
{
if(in_array($appname, array_keys($GLOBALS['egw_info']['apps']))) {
if(!in_array($appname, array_keys($GLOBALS['egw_info']['user']['apps'])))
{
unset($list[$appname]);
}
}
}
return $list;
}
/**
* Create an action to set the portlet as default
*
* @param Array $actions Existing action list
* @param String $portlet_id Provide the ID to have the checkbox set
*/
protected static function create_default_actions(&$actions, $portlet_id = null)
{
if($GLOBALS['egw_info']['user']['apps']['admin'])
{
$actions['add_default'] = array(
'type' => 'popup',
'caption' => 'Set as default',
'onExecute' => 'javaScript:app.home.set_default',
'group' => 'Admins',
'icon' => 'preference'
);
// Customize for the given portlet
if($portlet_id !== null)
{
foreach(array('forced','group','default') as $location)
{
$loc = $GLOBALS['egw']->preferences->$location;
if($loc['home'][$portlet_id])
{
// If it's forced, no point in setting default
if($location == 'forced')
{
unset($actions['add_default']);
}
// If it's a group, we'd like to know which
if($location == 'group')
{
2016-04-28 20:06:46 +02:00
$options = array('account_type' => 'groups');
$groups = Api\Accounts::link_query('',$options);
foreach(array_keys($groups) as $gid)
{
2016-04-28 20:06:46 +02:00
$prefs = new Api\Preferences($gid);
$prefs->read_repository();
if (isset($prefs->user['home'][$portlet_id]))
{
$location = $gid;
break;
}
}
}
$actions['remove_default_'.$location] = array(
'type' => 'popup',
2016-04-28 20:06:46 +02:00
'caption' => lang('Remove as default for %1',is_numeric($location) ? Api\Accounts::id2name($location,'account_fullname') : $location),
'onExecute' => 'javaScript:app.home.set_default',
'group' => 'Admins',
'portlet_group' => $location
);
}
}
}
}
// Change action for forced
if($portlet_id && $GLOBALS['egw']->preferences->forced['home'][$portlet_id])
{
// No one can remove it
$actions['remove_portlet']['enabled'] = false;
$actions['remove_portlet']['caption'] .= ' ('.lang('Forced') .')';
// Non-admins can't edit it
if($actions['edit_settings'] && !$GLOBALS['egw_info']['user']['apps']['admin'])
{
$actions['edit_settings']['enabled'] = false;
$actions['edit_settings']['visible'] = false;
}
}
}
/**
* Update the settings for a particular portlet, and give updated content
*
* @param portlet_id String Unique ID (for the user) for a portlet
* @param values Array List of property => value pairs
* @param boolean|int|String $group False for current user, ID of the group to create the favorite for, or 'all' for all users
*
*/
public function ajax_set_properties($portlet_id, $attributes, $values, $group = false)
{
2015-01-16 19:08:10 +01:00
//error_log(__METHOD__ . "($portlet_id, " .array2string($attributes).','.array2string($values).",$group)");
if(!$attributes)
{
$attributes = array();
}
if(!$GLOBALS['egw_info']['user']['apps']['admin'])
{
if($group == 'forced')
{
// Quietly reject
return;
}
// Not an admin, can only override.
$group = false;
}
if($group && $GLOBALS['egw_info']['user']['apps']['admin'])
{
2016-04-28 20:06:46 +02:00
$prefs = new Api\Preferences(is_numeric($group) ? $group : $GLOBALS['egw_info']['user']['account_id']);
}
else
{
$prefs = $GLOBALS['egw']->preferences;
}
$type = is_numeric($group) ? "user" : $group;
$prefs->read_repository();
2016-04-28 20:06:46 +02:00
$response = Api\Json\Response::get();
if($values =='~reload~')
{
$full_exec = true;
$values = array();
}
if($values == '~remove~')
{
// Already removed client side, needs to be removed permanently
$default = $prefs->default_prefs('home',$portlet_id) || $prefs->group['home'][$portlet_id];
if($default)
{
// Can't delete forced - not a UI option though
if(!$GLOBALS['egw']->preferences->forced['home'][$portlet_id])
{
// Set a flag to override default instead of just delete
$GLOBALS['egw']->preferences->add('home',$portlet_id, 'deleted');
$GLOBALS['egw']->preferences->save_repository();
}
}
else
{
$prefs->delete('home', $portlet_id);
$response->data([]);
}
}
else
{
$pref_data = $prefs->read();
$portlets = $pref_data['home'];
// Remove some constant stuff that winds up here
unset($values['edit_template']);
unset($values['readonly']);
unset($values['disabled']);unset($values['no_lang']);
unset($values['actions']);
unset($values['statustext']);
unset($values['type']);unset($values['label']);unset($values['status']);
unset($values['value']);unset($values['align']);
// Get portlet settings, and merge new with old
2015-01-12 21:39:25 +01:00
$context = array_merge((array)$portlets[$portlet_id], $values);
$context['group'] = $group;
// Handle add IDs
$classname =& $context['class'];
if(strpos($classname,'add_') == 0 && !class_exists($classname))
{
$add = true;
$classname = substr($classname, 4);
}
2016-04-28 20:06:46 +02:00
$content = null;
$portlet = $this->get_portlet("home-index_portlets_" . $portlet_id, $context, $content, $attributes, $full_exec);
2014-11-06 22:40:03 +01:00
$context['class'] = get_class($portlet);
foreach($portlet->get_properties() as $property)
{
if($values[$property['name']])
{
$context[$property['name']] = $values[$property['name']];
}
elseif($portlets[$portlet_id][$property['name']])
{
$context[$property['name']] = $portlets[$portlet_id][$property['name']];
}
}
// Update client side
$update = array('attributes' => $attributes);
// New portlet? Flag going straight to edit mode
if($add)
{
$update['edit_settings'] = true;
}
// Send this back to the portlet widget
$response->data($update);
// Store for preference update
$prefs->add('home', $portlet_id, $context, $type);
}
// Save updated preferences
$prefs->save_repository(True,$type);
}
/**
* Set the selected portlets as default for a group
*
* @param String $action 'add' or 'delete'
* @param String[] $portlet_ids
* @param int|String $group Group ID or 'default' or 'forced'
*/
public static function ajax_set_default($action, $portlet_ids, $group)
{
// Admins only
if(!$GLOBALS['egw_info']['apps']['admin']) return;
// Load the appropriate group
if($group)
{
2016-04-28 20:06:46 +02:00
$prefs = new Api\Preferences(is_numeric($group) ? $group : $GLOBALS['egw_info']['user']['account_id']);
}
else
{
$prefs = $GLOBALS['egw']->preferences;
}
$prefs->read_repository();
$type = is_numeric($group) ? "user" : $group;
if($action == 'add')
{
foreach($portlet_ids as $id)
{
2016-04-28 20:06:46 +02:00
Api\Json\Response::get()->call('egw.message', lang("Set default"));
// Current user is setting the default, copy their settings
$settings = $GLOBALS['egw_info']['user']['preferences']['home'][$id];
$settings['group'] = $group;
$prefs->add('home',$id,$settings,$type);
// Remove user's copy
$GLOBALS['egw']->preferences->delete('home',$id);
$GLOBALS['egw']->preferences->save_repository(true);
}
}
else if ($action == "delete")
{
foreach($portlet_ids as $id)
{
2016-04-28 20:06:46 +02:00
Api\Json\Response::get()->call('egw.message', lang("Removed default"));
$prefs->delete('home',$id, $type);
}
}
$prefs->save_repository(false,$type);
2016-04-28 20:06:46 +02:00
// Update Api\Preferences client side for consistency
$prefs = $GLOBALS['egw']->preferences;
$pref = $prefs->read_repository();
2016-04-28 20:06:46 +02:00
Api\Json\Response::get()->call('egw.set_preferences', (array)$pref['home'], 'home');
}
/**
* Name of config var to store installed default home screen
*/
const HOME_VERSION = 'home_version';
/**
* Current version of home screen, there need to be a function matching that number!
*/
const CURRENT_HOME_VERSION = '14.3';
/**
* Setup default home screen for 14.2
*/
public static function setup_default_home_14_3()
{
$preferences = $GLOBALS['egw']->preferences;
$preferences->read_repository();
$lang = $preferences->default['common']['lang'];
if (empty($lang)) $lang = 'en';
2016-04-28 20:06:46 +02:00
Api\Translation::add_app('calendar', $lang);
$weekview = lang('Weekview');
if ($GLOBALS['egw_info']['apps']['news_admin'])
{
2016-04-28 20:06:46 +02:00
$cats = new Api\Categories('', 'news_admin');
$cat_id_egw_org_news = $cats->name2id('egroupware.org');
}
$app_prefs = array(
'home' => array(
// current users week favorite
'portlet_setup142w' => array(
'id' => 'portlet_setup-142wp',
'class' => 'calendar_favorite_portlet',
'row' => 7,
'col' => 1,
'width' => 7,
'height' => 5,
'color' => '',
'favorite' => array(
'name' => $weekview,
'group' => 'default',
'state' => array(
'cat_id' => '0',
'filter' => 'default',
'owner' => 0, // current user
'sortby' => 'user',
'planner_days' => '0',
'view' => 'week',
'listview_days' => '',
),
),
'title' => $weekview,
'group' => 'default',
'appname' => 'calendar',
),
// egroupware.org news
'portlet_setup142n' => array(
'row' => 7,
'col' => 8,
'width' => 8,
'height' => 5,
'color' => '',
'favorite' => array(
'name' => 'www.egroupware.org',
'group' => 'default',
'state' => array(
'selected' => array(),
'col_filter' => array(
'visible' => 'now',
'news_lang' => $lang == 'de' ? 'de' : 'en',
'news_submittedby' => ''
),
'filter' => $cat_id_egw_org_news,
'filter2' => 'content',
'search' => '',
'sort' => array(
'id' => 'news_date',
'asc' => false
),
),
),
'id' => 'portlet_setup142n',
'class' => 'news_admin_favorite_portlet',
'title' => 'www.egroupware.org',
'group' => 'default',
'appname' => 'news_admin'
),
),
);
if (empty($cat_id_egw_org_news))
{
unset($app_prefs['home']['portlet_setup142n']);
}
foreach($app_prefs as $app => $prefs)
{
foreach($prefs as $name => $value)
{
2016-04-28 20:06:46 +02:00
Api\Preferences::delete_preference($app, $name, 'default');
$preferences->add($app, $name, $value, 'default');
}
}
// remove tutorial from home, as it is now in sidebox
2016-04-28 20:06:46 +02:00
Api\Preferences::delete_preference('home', 'portlet_setup142t', 'default');
$preferences->delete('home', 'portlet_setup142t', 'default');
// assigning saved preferences to egw_info, which is used for this request
$GLOBALS['egw_info']['user']['preferences'] = $preferences->save_repository(null, 'default');
}
/**
* Check and if necessary setup default home screen
*
* Version of home-screen is stored in api config self::HOME_VERSION
*/
public static function setup_default_home()
{
switch($GLOBALS['egw_info']['server'][self::HOME_VERSION])
{
case self::CURRENT_HOME_VERSION:
return; // already up to date --> nothing to do
default:
call_user_func(array(__CLASS__, 'setup_default_home_'.str_replace('.', '_', self::CURRENT_HOME_VERSION)));
2016-04-28 20:06:46 +02:00
Api\Config::save_value(self::HOME_VERSION, self::CURRENT_HOME_VERSION, 'phpgwapi');
break;
}
}
}