Commit 55f3a9f1 authored by Rafaël Carré's avatar Rafaël Carré

Interaction are controlled by a dedicated thread

This is not the playlist's work at all
Fix #1520
parent f0c27947
......@@ -20,7 +20,6 @@ set( SOURCES_libvlc_common
libvlc-common.c
libvlc.h
libvlc-module.c
interface/interface.h
interface/interface.c
interface/intf_eject.c
interface/interaction.c
......
......@@ -67,6 +67,7 @@
#define VLC_OBJECT_STATS (-29)
#define VLC_OBJECT_HTTPD_HOST (-30)
#define VLC_OBJECT_META_ENGINE (-31)
#define VLC_OBJECT_INTERACTION (-32)
#define VLC_OBJECT_GENERIC (-666)
......
......@@ -237,7 +237,6 @@ struct playlist_t
} request;
// Playlist-unrelated fields
interaction_t *p_interaction; /**< Interaction manager */
input_thread_t *p_stats_computer; /**< Input thread computing stats */
global_stats_t *p_stats; /**< Global statistics */
};
......
......@@ -235,7 +235,6 @@ SOURCES_libvlc_common = \
libvlc-common.c \
libvlc.h \
libvlc-module.c \
interface/interface.h \
interface/interface.c \
interface/intf_eject.c \
interface/interaction.c \
......
/*****************************************************************************
* interaction.c: User interaction functions
*****************************************************************************
* Copyright (C) 2005-2006 the VideoLAN team
* Copyright © 2005-2008 the VideoLAN team
* $Id$
*
* Authors: Clément Stenac <zorglub@videolan.org>
......@@ -37,148 +37,36 @@
#include <vlc/vlc.h>
#include <stdlib.h> /* free(), strtol() */
#include <stdio.h> /* FILE */
#include <string.h>
#include <vlc_interface.h>
#include <vlc_playlist.h>
#include "interface.h"
/*****************************************************************************
* Local prototypes
*****************************************************************************/
static void InteractionInit( playlist_t *p_playlist );
static interaction_t * InteractionGet( vlc_object_t *p_this );
static void InteractionSearchInterface( interaction_t *
p_interaction );
static interaction_dialog_t *DialogGetById( interaction_t* , int );
static void DialogDestroy( interaction_dialog_t *p_dialog );
static int DialogSend( vlc_object_t *p_this, interaction_dialog_t *p_dialog );
/**
* Destroy the interaction system
*
* \param The interaction object to destroy
* \return nothing
*/
void intf_InteractionDestroy( interaction_t *p_interaction )
{
int i;
// Remove all dialogs - Interfaces must be able to clean up their data
for( i = p_interaction->i_dialogs -1 ; i >= 0; i-- )
{
interaction_dialog_t * p_dialog = p_interaction->pp_dialogs[i];
DialogDestroy( p_dialog );
REMOVE_ELEM( p_interaction->pp_dialogs, p_interaction->i_dialogs, i );
}
vlc_object_release( p_interaction );
}
/**
* The main interaction processing loop
* This function is called from the playlist loop
*
* \param p_playlist the parent playlist
* \return nothing
*/
void intf_InteractionManage( playlist_t *p_playlist )
{
vlc_value_t val;
int i_index;
interaction_t *p_interaction = p_playlist->p_interaction;
// Nothing to do
if( p_interaction->i_dialogs == 0 ) return;
vlc_mutex_lock( &p_interaction->object_lock );
static interaction_t * InteractionInit( libvlc_int_t * );
static interaction_t * InteractionGet( vlc_object_t * );
static void InteractionSearchInterface( interaction_t * );
static void InteractionLoop( vlc_object_t * );
static void InteractionManage( interaction_t * );
InteractionSearchInterface( p_interaction );
if( !p_interaction->p_intf )
{
// We mark all dialogs as answered with their "default" answer
for( i_index = 0 ; i_index < p_interaction->i_dialogs; i_index ++ )
{
interaction_dialog_t *p_dialog = p_interaction->pp_dialogs[i_index];
p_dialog->i_return = DIALOG_DEFAULT; // Give default answer
// Pretend we have hidden and destroyed it
if( p_dialog->i_status == HIDDEN_DIALOG )
p_dialog->i_status = DESTROYED_DIALOG;
else
p_dialog->i_status = HIDING_DIALOG;
}
}
else
vlc_object_yield( p_interaction->p_intf );
for( i_index = 0 ; i_index < p_interaction->i_dialogs; i_index ++ )
{
interaction_dialog_t *p_dialog = p_interaction->pp_dialogs[i_index];
switch( p_dialog->i_status )
{
case ANSWERED_DIALOG:
// Ask interface to hide it
p_dialog->i_action = INTERACT_HIDE;
val.p_address = p_dialog;
if( p_interaction->p_intf )
var_Set( p_interaction->p_intf, "interaction", val );
p_dialog->i_status = HIDING_DIALOG;
break;
case UPDATED_DIALOG:
p_dialog->i_action = INTERACT_UPDATE;
val.p_address = p_dialog;
if( p_interaction->p_intf )
var_Set( p_interaction->p_intf, "interaction", val );
p_dialog->i_status = SENT_DIALOG;
break;
case HIDDEN_DIALOG:
if( !(p_dialog->i_flags & DIALOG_GOT_ANSWER) ) break;
p_dialog->i_action = INTERACT_DESTROY;
val.p_address = p_dialog;
if( p_interaction->p_intf )
var_Set( p_interaction->p_intf, "interaction", val );
break;
case DESTROYED_DIALOG:
// Interface has now destroyed it, remove it
REMOVE_ELEM( p_interaction->pp_dialogs, p_interaction->i_dialogs,
i_index);
i_index--;
DialogDestroy( p_dialog );
break;
case NEW_DIALOG:
// This is truly a new dialog, send it.
p_dialog->i_action = INTERACT_NEW;
val.p_address = p_dialog;
if( p_interaction->p_intf )
var_Set( p_interaction->p_intf, "interaction", val );
p_dialog->i_status = SENT_DIALOG;
break;
}
}
if( p_interaction->p_intf )
{
vlc_object_release( p_interaction->p_intf );
}
vlc_mutex_unlock( &p_playlist->p_interaction->object_lock );
}
static interaction_dialog_t *DialogGetById( interaction_t* , int );
static void DialogDestroy( interaction_dialog_t * );
static int DialogSend( vlc_object_t *, interaction_dialog_t * );
#define DIALOG_INIT( type ) \
DECMALLOC_ERR( p_new, interaction_dialog_t ); \
memset( p_new, 0, sizeof( interaction_dialog_t ) ); \
p_new->b_cancelled = VLC_FALSE; \
p_new->i_status = NEW_DIALOG; \
p_new->i_flags = 0; \
p_new->i_type = INTERACT_DIALOG_##type; \
p_new->psz_returned[0] = NULL; \
p_new->psz_returned[1] = NULL;
DECMALLOC_ERR( p_new, interaction_dialog_t ); \
memset( p_new, 0, sizeof( interaction_dialog_t ) ); \
p_new->b_cancelled = VLC_FALSE; \
p_new->i_status = NEW_DIALOG; \
p_new->i_flags = 0; \
p_new->i_type = INTERACT_DIALOG_##type; \
p_new->psz_returned[0] = NULL; \
p_new->psz_returned[1] = NULL
#define FORMAT_DESC \
va_start( args, psz_format ); \
vasprintf( &p_new->psz_description, psz_format, args ); \
va_end( args );
if( vasprintf( &p_new->psz_description, psz_format, args ) == -1 ) \
return VLC_EGENERIC; \
va_end( args )
/**
* Send an error message, both in a blocking and non-blocking way
......@@ -223,7 +111,7 @@ int __intf_UserWarn( vlc_object_t *p_this,
DIALOG_INIT( ONEWAY );
p_new->psz_title = strdup( psz_title );
FORMAT_DESC
FORMAT_DESC;
p_new->i_flags = DIALOG_WARNING;
......@@ -310,12 +198,13 @@ void __intf_ProgressUpdate( vlc_object_t *p_this, int i_id,
if( !p_interaction ) return;
vlc_mutex_lock( &p_interaction->object_lock );
vlc_object_lock( p_interaction );
p_dialog = DialogGetById( p_interaction, i_id );
if( !p_dialog )
{
vlc_mutex_unlock( &p_interaction->object_lock ) ;
vlc_object_unlock( p_interaction );
vlc_object_release( p_interaction );
return;
}
......@@ -326,9 +215,10 @@ void __intf_ProgressUpdate( vlc_object_t *p_this, int i_id,
p_dialog->i_timeToGo = i_time;
p_dialog->i_status = UPDATED_DIALOG;
vlc_mutex_unlock( &p_interaction->object_lock) ;
playlist_Signal( pl_Get( p_this ) );
vlc_object_signal_unlocked( p_interaction );
vlc_object_unlock( p_interaction );
vlc_object_release( p_interaction );
}
/**
......@@ -347,16 +237,18 @@ vlc_bool_t __intf_UserProgressIsCancelled( vlc_object_t *p_this, int i_id )
if( !p_interaction ) return VLC_TRUE;
vlc_mutex_lock( &p_interaction->object_lock );
vlc_object_lock( p_interaction );
p_dialog = DialogGetById( p_interaction, i_id );
if( !p_dialog )
{
vlc_mutex_unlock( &p_interaction->object_lock ) ;
vlc_object_unlock( p_interaction ) ;
vlc_object_release( p_interaction );
return VLC_TRUE;
}
b_cancel = p_dialog->b_cancelled;
vlc_mutex_unlock( &p_interaction->object_lock );
vlc_object_unlock( p_interaction );
vlc_object_release( p_interaction );
return b_cancel;
}
......@@ -371,10 +263,10 @@ vlc_bool_t __intf_UserProgressIsCancelled( vlc_object_t *p_this, int i_id )
* \return Clicked button code
*/
int __intf_UserLoginPassword( vlc_object_t *p_this,
const char *psz_title,
const char *psz_description,
char **ppsz_login,
char **ppsz_password )
const char *psz_title,
const char *psz_description,
char **ppsz_login,
char **ppsz_password )
{
int i_ret;
DIALOG_INIT( TWOWAY );
......@@ -391,9 +283,9 @@ int __intf_UserLoginPassword( vlc_object_t *p_this,
if( i_ret != DIALOG_CANCELLED && i_ret != VLC_EGENERIC )
{
*ppsz_login = p_new->psz_returned[0]?
strdup( p_new->psz_returned[0] ) : NULL;
strdup( p_new->psz_returned[0] ) : NULL;
*ppsz_password = p_new->psz_returned[1]?
strdup( p_new->psz_returned[1] ) : NULL;
strdup( p_new->psz_returned[1] ) : NULL;
}
return i_ret;
}
......@@ -408,9 +300,9 @@ int __intf_UserLoginPassword( vlc_object_t *p_this,
* \return Clicked button code
*/
int __intf_UserStringInput( vlc_object_t *p_this,
const char *psz_title,
const char *psz_description,
char **ppsz_usersString )
const char *psz_title,
const char *psz_description,
char **ppsz_usersString )
{
int i_ret;
DIALOG_INIT( TWOWAY );
......@@ -425,7 +317,7 @@ int __intf_UserStringInput( vlc_object_t *p_this,
if( i_ret != DIALOG_CANCELLED )
{
*ppsz_usersString = p_new->psz_returned[0]?
strdup( p_new->psz_returned[0] ) : NULL;
strdup( p_new->psz_returned[0] ) : NULL;
}
return i_ret;
}
......@@ -444,17 +336,14 @@ void __intf_UserHide( vlc_object_t *p_this, int i_id )
if( !p_interaction ) return;
vlc_mutex_lock( &p_interaction->object_lock );
vlc_object_lock( p_interaction );
p_dialog = DialogGetById( p_interaction, i_id );
if( !p_dialog )
{
vlc_mutex_unlock( &p_interaction->object_lock );
return;
}
if( p_dialog )
p_dialog->i_status = ANSWERED_DIALOG;
p_dialog->i_status = ANSWERED_DIALOG;
vlc_mutex_unlock( &p_interaction->object_lock );
vlc_object_unlock( p_interaction );
vlc_object_release( p_interaction );
}
/**********************************************************************
......@@ -464,39 +353,45 @@ void __intf_UserHide( vlc_object_t *p_this, int i_id )
/* Get the interaction object. Create it if needed */
static interaction_t * InteractionGet( vlc_object_t *p_this )
{
interaction_t *p_interaction;
playlist_t *p_playlist = pl_Yield( p_this );
PL_LOCK;
if( p_playlist->p_interaction == NULL )
InteractionInit( p_playlist );
interaction_t *p_interaction =
vlc_object_find( p_this, VLC_OBJECT_INTERACTION, FIND_ANYWHERE );
p_interaction = p_playlist->p_interaction;
PL_UNLOCK;
if( !p_interaction )
p_interaction = InteractionInit( p_this->p_libvlc );
pl_Release( p_this );
return p_interaction;
}
/* Create the interaction object in the given playlist object */
static void InteractionInit( playlist_t *p_playlist )
static interaction_t * InteractionInit( libvlc_int_t *p_libvlc )
{
interaction_t *p_interaction = vlc_object_create( VLC_OBJECT( p_playlist ),
sizeof( interaction_t ) );
if( !p_interaction )
interaction_t *p_interaction =
vlc_object_create( p_libvlc, VLC_OBJECT_INTERACTION );
if( p_interaction )
{
msg_Err( p_playlist,"out of memory" );
return;
vlc_object_attach( p_interaction, p_libvlc );
p_interaction->i_dialogs = 0;
p_interaction->pp_dialogs = NULL;
p_interaction->p_intf = NULL;
p_interaction->i_last_id = 0;
if( vlc_thread_create( p_interaction, "Interaction control",
InteractionLoop, VLC_THREAD_PRIORITY_LOW,
VLC_FALSE ) )
{
msg_Err( p_interaction, "Interaction control thread creation failed"
", interaction will not be displayed" );
vlc_object_detach( p_interaction );
vlc_object_release( p_interaction );
p_interaction = NULL;
}
else
vlc_object_yield( p_interaction );
}
p_interaction->psz_object_name = "interaction";
p_interaction->i_dialogs = 0;
p_interaction->pp_dialogs = NULL;
p_interaction->p_intf = NULL;
p_interaction->i_last_id = 0;
vlc_mutex_init( p_interaction , &p_interaction->object_lock );
p_playlist->p_interaction = p_interaction;
return p_interaction;
}
/* Look for an interface suitable for interaction */
......@@ -573,7 +468,7 @@ static int DialogSend( vlc_object_t *p_this, interaction_dialog_t *p_dialog )
p_dialog->p_parent = p_this;
/* Check if we have already added this dialog */
vlc_mutex_lock( &p_interaction->object_lock );
vlc_object_lock( p_interaction );
for( i = 0 ; i< p_interaction->i_dialogs; i++ )
{
if( p_interaction->pp_dialogs[i]->i_id == p_dialog->i_id )
......@@ -591,17 +486,17 @@ static int DialogSend( vlc_object_t *p_this, interaction_dialog_t *p_dialog )
else
p_dialog->i_status = UPDATED_DIALOG;
if( p_dialog->i_type == INTERACT_DIALOG_TWOWAY ) // Wait for answer
if( p_dialog->i_type == INTERACT_DIALOG_TWOWAY ) /* Wait for answer */
{
playlist_Signal( pl_Get( p_this ) );
vlc_object_signal_unlocked( p_interaction );
while( p_dialog->i_status != ANSWERED_DIALOG &&
p_dialog->i_status != HIDING_DIALOG &&
p_dialog->i_status != HIDDEN_DIALOG &&
!p_dialog->p_parent->b_die )
{
vlc_mutex_unlock( &p_interaction->object_lock );
vlc_object_unlock( p_interaction );
msleep( 100000 );
vlc_mutex_lock( &p_interaction->object_lock );
vlc_object_lock( p_interaction );
}
if( p_dialog->p_parent->b_die )
{
......@@ -609,19 +504,137 @@ static int DialogSend( vlc_object_t *p_this, interaction_dialog_t *p_dialog )
p_dialog->i_status = ANSWERED_DIALOG;
}
p_dialog->i_flags |= DIALOG_GOT_ANSWER;
vlc_mutex_unlock( &p_interaction->object_lock );
playlist_Signal( pl_Get( p_this ) );
vlc_object_signal_unlocked( p_interaction );
vlc_object_unlock( p_interaction );
vlc_object_release( p_interaction );
return p_dialog->i_return;
}
else
{
// Pretend we already retrieved the "answer"
/* Pretend we already retrieved the "answer" */
p_dialog->i_flags |= DIALOG_GOT_ANSWER;
vlc_mutex_unlock( &p_interaction->object_lock );
playlist_Signal( pl_Get( p_this ) );
vlc_object_signal_unlocked( p_interaction );
vlc_object_unlock( p_interaction );
vlc_object_release( p_interaction );
return VLC_SUCCESS;
}
}
else
{
vlc_object_release( p_interaction );
return VLC_EGENERIC;
}
}
static void InteractionLoop( vlc_object_t *p_this )
{
int i;
interaction_t *p_interaction = (interaction_t*) p_this;
while( !p_this->b_die )
{
vlc_object_lock( p_this );
if( vlc_object_wait( p_this ) )
{
vlc_object_unlock( p_this );
break;
}
InteractionManage( p_interaction );
vlc_object_unlock( p_this );
}
/* Remove all dialogs - Interfaces must be able to clean up their data */
for( i = p_interaction->i_dialogs -1 ; i >= 0; i-- )
{
interaction_dialog_t * p_dialog = p_interaction->pp_dialogs[i];
DialogDestroy( p_dialog );
REMOVE_ELEM( p_interaction->pp_dialogs, p_interaction->i_dialogs, i );
}
vlc_object_detach( p_this );
vlc_object_release( p_this );
}
/**
* The main interaction processing loop
*
* \param p_interaction the interaction object
* \return nothing
*/
static void InteractionManage( interaction_t *p_interaction )
{
vlc_value_t val;
int i_index;
/* Nothing to do */
if( p_interaction->i_dialogs == 0 ) return;
InteractionSearchInterface( p_interaction );
if( !p_interaction->p_intf )
{
/* We mark all dialogs as answered with their "default" answer */
for( i_index = 0 ; i_index < p_interaction->i_dialogs; i_index ++ )
{
interaction_dialog_t *p_dialog = p_interaction->pp_dialogs[i_index];
p_dialog->i_return = DIALOG_DEFAULT; /* Give default answer */
/* Pretend we have hidden and destroyed it */
if( p_dialog->i_status == HIDDEN_DIALOG )
p_dialog->i_status = DESTROYED_DIALOG;
else
p_dialog->i_status = HIDING_DIALOG;
}
}
else
vlc_object_yield( p_interaction->p_intf );
for( i_index = 0 ; i_index < p_interaction->i_dialogs; i_index ++ )
{
interaction_dialog_t *p_dialog = p_interaction->pp_dialogs[i_index];
switch( p_dialog->i_status )
{
case ANSWERED_DIALOG:
/* Ask interface to hide it */
p_dialog->i_action = INTERACT_HIDE;
val.p_address = p_dialog;
if( p_interaction->p_intf )
var_Set( p_interaction->p_intf, "interaction", val );
p_dialog->i_status = HIDING_DIALOG;
break;
case UPDATED_DIALOG:
p_dialog->i_action = INTERACT_UPDATE;
val.p_address = p_dialog;
if( p_interaction->p_intf )
var_Set( p_interaction->p_intf, "interaction", val );
p_dialog->i_status = SENT_DIALOG;
break;
case HIDDEN_DIALOG:
if( !(p_dialog->i_flags & DIALOG_GOT_ANSWER) ) break;
p_dialog->i_action = INTERACT_DESTROY;
val.p_address = p_dialog;
if( p_interaction->p_intf )
var_Set( p_interaction->p_intf, "interaction", val );
break;
case DESTROYED_DIALOG:
/* Interface has now destroyed it, remove it */
REMOVE_ELEM( p_interaction->pp_dialogs, p_interaction->i_dialogs,
i_index);
i_index--;
DialogDestroy( p_dialog );
break;
case NEW_DIALOG:
/* This is truly a new dialog, send it. */
p_dialog->i_action = INTERACT_NEW;
val.p_address = p_dialog;
if( p_interaction->p_intf )
var_Set( p_interaction->p_intf, "interaction", val );
p_dialog->i_status = SENT_DIALOG;
break;
}
}
if( p_interaction->p_intf )
vlc_object_release( p_interaction->p_intf );
}
/*****************************************************************************
* interface.h: Internal interface prototypes and structures
*****************************************************************************
* Copyright (C) 1998-2006 the VideoLAN team
* $Id$
*
* Authors: Vincent Seguin <seguin@via.ecp.fr>
* Clément Stenac <zorglub@videolan.org>
* Felix Kühne <fkuehne@videolan.org>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
*****************************************************************************/
#if defined(__PLUGIN__) || defined(__BUILTIN__) || !defined(__LIBVLC__)
# error This header file can only be included from LibVLC.
#endif
#ifndef __LIBVLC_INTERFACE_H
# define __LIBVLC_INTERFACE_H 1
/**********************************************************************
* Interaction
**********************************************************************/
void intf_InteractionManage( playlist_t *);
void intf_InteractionDestroy( interaction_t *);
#endif
......@@ -336,6 +336,10 @@ void * __vlc_object_create( vlc_object_t *p_this, int i_type )
i_size = sizeof( osd_menu_t );
psz_type = "osd menu";
break;
case VLC_OBJECT_INTERACTION:
i_size = sizeof( interaction_t );
psz_type = "interaction";
break;
default:
i_size = i_type > (int)sizeof(vlc_object_t)
? i_type : (int)sizeof(vlc_object_t);
......
......@@ -151,11 +151,6 @@ playlist_t * playlist_Create( vlc_object_t *p_parent )
p_playlist->i_order = ORDER_NORMAL;
/* Interaction
* Init the interaction here, as playlist_MLLoad could trigger
* interaction init */
p_playlist->p_interaction = NULL;
b_save = p_playlist->b_auto_preparse;
p_playlist->b_auto_preparse = VLC_FALSE;
playlist_MLLoad( p_playlist );
......
/*****************************************************************************
* playlist.c : Playlist management functions
* thread.c : Playlist management functions
*****************************************************************************
* Copyright (C) 1999-2004 the VideoLAN team
* Copyright © 1999-2008 the VideoLAN team
* $Id$
*
* Authors: Samuel Hocevar <sam@zoy.org>
......@@ -31,7 +31,6 @@
#include <vlc_interface.h>
#include <vlc_playlist.h>
#include "playlist_internal.h"
#include "interface/interface.h"
/*****************************************************************************
* Local prototypes
......@@ -40,8 +39,6 @@ static void RunControlThread ( playlist_t * );
static void RunPreparse( playlist_preparse_t * );
static void RunFetcher( playlist_fetcher_t * );
static void DestroyInteraction( playlist_t * );
/*****************************************************************************
* Main functions for the global thread
*****************************************************************************/
......@@ -49,7 +46,6 @@ static void DestroyInteraction( playlist_t * );
/**
* Create the main playlist thread
* Additionally to the playlist, this thread controls :
* - Interaction
* - Statistics
* - VLM
* \param p_parent
......@@ -175,8 +171,6 @@ int playlist_ThreadDestroy( playlist_t * p_playlist )
if( p_playlist->p_stats )
free( p_playlist->p_stats );
DestroyInteraction( p_playlist );
playlist_Destroy( p_playlist );
return VLC_SUCCESS;
......@@ -195,9 +189,6 @@ static void RunControlThread ( playlist_t *p_playlist )
{
i_loops++;
if( p_playlist->p_interaction )
intf_InteractionManage( p_playlist );
playlist_MainLoop( p_playlist );
if( p_playlist->b_cant_sleep )
{
......@@ -230,15 +221,3 @@ static void RunFetcher( playlist_fetcher_t *p_obj )
vlc_thread_ready( p_obj );
playlist_FetcherLoop( p_obj );
}
/*****************************************************************************
* Interaction functions
*****************************************************************************/
static void DestroyInteraction( playlist_t *p_playlist )
{
if( p_playlist->p_interaction )
{
intf_InteractionDestroy( p_playlist->p_interaction );
p_playlist->p_interaction = NULL;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment