mirror of
https://gitlab.linphone.org/BC/public/linphone-iphone.git
synced 2026-02-01 18:59:25 +00:00
Implement RFC3994: Indication of Message Composition for Instant Messaging.
This commit is contained in:
parent
fe1ca6f07c
commit
d468050c8b
20 changed files with 552 additions and 145 deletions
|
|
@ -13,7 +13,8 @@ COMMON_CFLAGS=\
|
|||
$(MEDIASTREAMER_CFLAGS) \
|
||||
$(VIDEO_CFLAGS) \
|
||||
$(READLINE_CFLAGS) \
|
||||
$(SQLITE3_CFLAGS)
|
||||
$(SQLITE3_CFLAGS) \
|
||||
$(LIBXML2_CFLAGS)
|
||||
|
||||
if BUILD_CONSOLE
|
||||
|
||||
|
|
@ -29,7 +30,8 @@ linphonec_LDADD=$(top_builddir)/coreapi/liblinphone.la \
|
|||
$(READLINE_LIBS) \
|
||||
$(SQLITE3_LIBS) \
|
||||
$(X11_LIBS) \
|
||||
$(BELLESIP_LIBS)
|
||||
$(BELLESIP_LIBS) \
|
||||
$(LIBXML2_LIBS)
|
||||
|
||||
if BUILD_WIN32
|
||||
#special build of linphonec to detach from the windows console
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ liblinphone_la_SOURCES=\
|
|||
contactprovider.c contactprovider.h contact_providers_priv.h \
|
||||
ldap/ldapprovider.c ldap/ldapprovider.h \
|
||||
dict.c \
|
||||
xml.c \
|
||||
$(GITVERSION_FILE)
|
||||
|
||||
if BUILD_UPNP
|
||||
|
|
|
|||
|
|
@ -464,6 +464,8 @@ void sal_set_callbacks(Sal *ctx, const SalCallbacks *cbs){
|
|||
ctx->callbacks.subscribe_presence_received=(SalOnSubscribePresenceReceived)unimplemented_stub;
|
||||
if (ctx->callbacks.text_received==NULL)
|
||||
ctx->callbacks.text_received=(SalOnTextReceived)unimplemented_stub;
|
||||
if (ctx->callbacks.is_composing_received==NULL)
|
||||
ctx->callbacks.is_composing_received=(SalOnIsComposingReceived)unimplemented_stub;
|
||||
if (ctx->callbacks.ping_reply==NULL)
|
||||
ctx->callbacks.ping_reply=(SalOnPingReply)unimplemented_stub;
|
||||
if (ctx->callbacks.auth_requested==NULL)
|
||||
|
|
@ -915,5 +917,12 @@ unsigned char * sal_get_random_bytes(unsigned char *ret, size_t size){
|
|||
return belle_sip_random_bytes(ret,size);
|
||||
}
|
||||
|
||||
belle_sip_source_t * sal_create_timer(Sal *sal, belle_sip_source_func_t func, void *data, unsigned int timeout_value_ms, const char* timer_name) {
|
||||
belle_sip_main_loop_t *ml = belle_sip_stack_get_main_loop(sal->stack);
|
||||
return belle_sip_main_loop_create_timeout(ml, func, data, timeout_value_ms, timer_name);
|
||||
}
|
||||
|
||||
|
||||
void sal_cancel_timer(Sal *sal, belle_sip_source_t *timer) {
|
||||
belle_sip_main_loop_t *ml = belle_sip_stack_get_main_loop(sal->stack);
|
||||
belle_sip_main_loop_remove_source(ml, timer);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,10 @@ static bool_t is_external_body(belle_sip_header_content_type_t* content_type) {
|
|||
return strcmp("message",belle_sip_header_content_type_get_type(content_type))==0
|
||||
&& strcmp("external-body",belle_sip_header_content_type_get_subtype(content_type))==0;
|
||||
}
|
||||
static bool_t is_im_iscomposing(belle_sip_header_content_type_t* content_type) {
|
||||
return strcmp("application",belle_sip_header_content_type_get_type(content_type))==0
|
||||
&& strcmp("im-iscomposing+xml",belle_sip_header_content_type_get_subtype(content_type))==0;
|
||||
}
|
||||
|
||||
static void process_request_event(void *op_base, const belle_sip_request_event_t *event) {
|
||||
SalOp* op = (SalOp*)op_base;
|
||||
|
|
@ -88,8 +92,6 @@ static void process_request_event(void *op_base, const belle_sip_request_event_t
|
|||
belle_sip_header_call_id_t* call_id = belle_sip_message_get_header_by_type(req,belle_sip_header_call_id_t);
|
||||
belle_sip_header_cseq_t* cseq = belle_sip_message_get_header_by_type(req,belle_sip_header_cseq_t);
|
||||
belle_sip_header_date_t *date=belle_sip_message_get_header_by_type(req,belle_sip_header_date_t);
|
||||
SalMessage salmsg;
|
||||
char message_id[256]={0};
|
||||
int response_code=501;
|
||||
char* from;
|
||||
bool_t plain_text=FALSE;
|
||||
|
|
@ -99,7 +101,8 @@ static void process_request_event(void *op_base, const belle_sip_request_event_t
|
|||
content_type=belle_sip_message_get_header_by_type(BELLE_SIP_MESSAGE(req),belle_sip_header_content_type_t);
|
||||
if (content_type && ((plain_text=is_plain_text(content_type))
|
||||
|| (external_body=is_external_body(content_type)))) {
|
||||
|
||||
SalMessage salmsg;
|
||||
char message_id[256]={0};
|
||||
address=belle_sip_header_address_create(belle_sip_header_address_get_displayname(BELLE_SIP_HEADER_ADDRESS(from_header))
|
||||
,belle_sip_header_address_get_uri(BELLE_SIP_HEADER_ADDRESS(from_header)));
|
||||
from=belle_sip_object_to_string(BELLE_SIP_OBJECT(address));
|
||||
|
|
@ -121,6 +124,17 @@ static void process_request_event(void *op_base, const belle_sip_request_event_t
|
|||
belle_sip_free(from);
|
||||
if (salmsg.url) ms_free((char*)salmsg.url);
|
||||
response_code=200;
|
||||
} else if (content_type && is_im_iscomposing(content_type)) {
|
||||
SalIsComposing saliscomposing;
|
||||
address=belle_sip_header_address_create(belle_sip_header_address_get_displayname(BELLE_SIP_HEADER_ADDRESS(from_header))
|
||||
,belle_sip_header_address_get_uri(BELLE_SIP_HEADER_ADDRESS(from_header)));
|
||||
from=belle_sip_object_to_string(BELLE_SIP_OBJECT(address));
|
||||
saliscomposing.from=from;
|
||||
saliscomposing.text=belle_sip_message_get_body(BELLE_SIP_MESSAGE(req));
|
||||
op->base.root->callbacks.is_composing_received(op,&saliscomposing);
|
||||
belle_sip_object_unref(address);
|
||||
belle_sip_free(from);
|
||||
response_code=200;
|
||||
} else {
|
||||
ms_error("Unsupported MESSAGE with content type [%s/%s]",belle_sip_header_content_type_get_type(content_type)
|
||||
,belle_sip_header_content_type_get_subtype(content_type));
|
||||
|
|
|
|||
|
|
@ -843,6 +843,11 @@ static void text_received(SalOp *op, const SalMessage *msg){
|
|||
}
|
||||
}
|
||||
|
||||
static void is_composing_received(SalOp *op, const SalIsComposing *is_composing) {
|
||||
LinphoneCore *lc = (LinphoneCore *)sal_get_user_pointer(sal_op_get_sal(op));
|
||||
linphone_core_is_composing_received(lc, op, is_composing);
|
||||
}
|
||||
|
||||
static void parse_presence_requested(SalOp *op, const char *content_type, const char *content_subtype, const char *body, SalPresenceModel **result) {
|
||||
linphone_notify_parse_presence(op, content_type, content_subtype, body, result);
|
||||
}
|
||||
|
|
@ -1001,8 +1006,14 @@ static int op_equals(LinphoneCall *a, SalOp *b) {
|
|||
|
||||
static void text_delivery_update(SalOp *op, SalTextDeliveryStatus status){
|
||||
LinphoneChatMessage *chat_msg=(LinphoneChatMessage* )sal_op_get_user_pointer(op);
|
||||
const MSList* calls = linphone_core_get_calls(chat_msg->chat_room->lc);
|
||||
|
||||
const MSList* calls;
|
||||
|
||||
if (chat_msg == NULL) {
|
||||
// Do not handle delivery status for isComposing messages.
|
||||
return;
|
||||
}
|
||||
calls = linphone_core_get_calls(chat_msg->chat_room->lc);
|
||||
|
||||
chat_msg->state=chatStatusSal2Linphone(status);
|
||||
linphone_chat_message_store_state(chat_msg);
|
||||
if (chat_msg && chat_msg->cb) {
|
||||
|
|
@ -1123,6 +1134,7 @@ SalCallbacks linphone_sal_callbacks={
|
|||
refer_received,
|
||||
text_received,
|
||||
text_delivery_update,
|
||||
is_composing_received,
|
||||
notify_refer,
|
||||
subscribe_received,
|
||||
subscribe_closed,
|
||||
|
|
|
|||
246
coreapi/chat.c
246
coreapi/chat.c
|
|
@ -26,6 +26,12 @@
|
|||
#include "private.h"
|
||||
#include "lpconfig.h"
|
||||
|
||||
#include <libxml/xmlwriter.h>
|
||||
|
||||
#define COMPOSING_DEFAULT_IDLE_TIMEOUT 15
|
||||
#define COMPOSING_DEFAULT_REFRESH_TIMEOUT 60
|
||||
#define COMPOSING_DEFAULT_REMOTE_REFRESH_TIMEOUT 120
|
||||
|
||||
/**
|
||||
* @addtogroup chatroom
|
||||
* @{
|
||||
|
|
@ -85,13 +91,40 @@ LinphoneChatRoom* linphone_core_get_or_create_chat_room(LinphoneCore* lc, const
|
|||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
static void linphone_chat_room_delete_composing_idle_timer(LinphoneChatRoom *cr) {
|
||||
if (cr->composing_idle_timer) {
|
||||
sal_cancel_timer(cr->lc->sal, cr->composing_idle_timer);
|
||||
belle_sip_object_unref(cr->composing_idle_timer);
|
||||
cr->composing_idle_timer = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static void linphone_chat_room_delete_composing_refresh_timer(LinphoneChatRoom *cr) {
|
||||
if (cr->composing_refresh_timer) {
|
||||
sal_cancel_timer(cr->lc->sal, cr->composing_refresh_timer);
|
||||
belle_sip_object_unref(cr->composing_refresh_timer);
|
||||
cr->composing_refresh_timer = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static void linphone_chat_room_delete_remote_composing_refresh_timer(LinphoneChatRoom *cr) {
|
||||
if (cr->remote_composing_refresh_timer) {
|
||||
sal_cancel_timer(cr->lc->sal, cr->remote_composing_refresh_timer);
|
||||
belle_sip_object_unref(cr->remote_composing_refresh_timer);
|
||||
cr->remote_composing_refresh_timer = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a LinphoneChatRoom.
|
||||
* @param cr #LinphoneChatRoom object
|
||||
*/
|
||||
void linphone_chat_room_destroy(LinphoneChatRoom *cr){
|
||||
LinphoneCore *lc=cr->lc;
|
||||
linphone_chat_room_delete_composing_idle_timer(cr);
|
||||
linphone_chat_room_delete_composing_refresh_timer(cr);
|
||||
linphone_chat_room_delete_remote_composing_refresh_timer(cr);
|
||||
lc->chatrooms=ms_list_remove(lc->chatrooms,(void *) cr);
|
||||
linphone_address_destroy(cr->peer_url);
|
||||
ms_free(cr->peer);
|
||||
|
|
@ -142,6 +175,12 @@ static void _linphone_chat_room_send_message(LinphoneChatRoom *cr, LinphoneChatM
|
|||
msg->dir=LinphoneChatMessageOutgoing;
|
||||
msg->from=linphone_address_new(identity);
|
||||
msg->storage_id=linphone_chat_message_store(msg);
|
||||
|
||||
if (cr->is_composing == LinphoneIsComposingActive) {
|
||||
cr->is_composing = LinphoneIsComposingIdle;
|
||||
}
|
||||
linphone_chat_room_delete_composing_idle_timer(cr);
|
||||
linphone_chat_room_delete_composing_refresh_timer(cr);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -225,6 +264,89 @@ void linphone_core_message_received(LinphoneCore *lc, SalOp *op, const SalMessag
|
|||
ms_free(from);
|
||||
}
|
||||
|
||||
static int linphone_chat_room_remote_refresh_composing_expired(void *data, unsigned int revents) {
|
||||
LinphoneChatRoom *cr = (LinphoneChatRoom *)data;
|
||||
belle_sip_object_unref(cr->remote_composing_refresh_timer);
|
||||
cr->remote_composing_refresh_timer = NULL;
|
||||
cr->remote_is_composing = LinphoneIsComposingIdle;
|
||||
if (cr->lc->vtable.is_composing_received != NULL)
|
||||
cr->lc->vtable.is_composing_received(cr->lc, cr);
|
||||
return BELLE_SIP_STOP;
|
||||
}
|
||||
|
||||
static const char *iscomposing_prefix = "/xsi:isComposing";
|
||||
|
||||
static void process_im_is_composing_notification(LinphoneChatRoom *cr, xmlparsing_context_t *xml_ctx) {
|
||||
char xpath_str[MAX_XPATH_LENGTH];
|
||||
xmlXPathObjectPtr iscomposing_object;
|
||||
const char *state_str = NULL;
|
||||
const char *refresh_str = NULL;
|
||||
int refresh_duration = COMPOSING_DEFAULT_REMOTE_REFRESH_TIMEOUT;
|
||||
int i;
|
||||
LinphoneIsComposingState state = LinphoneIsComposingIdle;
|
||||
|
||||
if (linphone_create_xml_xpath_context(xml_ctx) < 0) return;
|
||||
|
||||
xmlXPathRegisterNs(xml_ctx->xpath_ctx, (const xmlChar *)"xsi", (const xmlChar *)"urn:ietf:params:xml:ns:im-iscomposing");
|
||||
iscomposing_object = linphone_get_xml_xpath_object_for_node_list(xml_ctx, iscomposing_prefix);
|
||||
if ((iscomposing_object != NULL) && (iscomposing_object->nodesetval != NULL)) {
|
||||
for (i = 1; i <= iscomposing_object->nodesetval->nodeNr; i++) {
|
||||
snprintf(xpath_str, sizeof(xpath_str), "%s[%i]/xsi:state", iscomposing_prefix, i);
|
||||
state_str = linphone_get_xml_text_content(xml_ctx, xpath_str);
|
||||
if (state_str == NULL) continue;
|
||||
snprintf(xpath_str, sizeof(xpath_str), "%s[%i]/xsi:refresh", iscomposing_prefix, i);
|
||||
refresh_str = linphone_get_xml_text_content(xml_ctx, xpath_str);
|
||||
}
|
||||
}
|
||||
|
||||
if (state_str != NULL) {
|
||||
if (strcmp(state_str, "active") == 0) {
|
||||
state = LinphoneIsComposingActive;
|
||||
if (refresh_str != NULL) {
|
||||
refresh_duration = atoi(refresh_str);
|
||||
}
|
||||
if (!cr->remote_composing_refresh_timer) {
|
||||
cr->remote_composing_refresh_timer = sal_create_timer(cr->lc->sal, linphone_chat_room_remote_refresh_composing_expired, cr, refresh_duration * 1000, "composing remote refresh timeout");
|
||||
} else {
|
||||
belle_sip_source_set_timeout(cr->remote_composing_refresh_timer, refresh_duration * 1000);
|
||||
}
|
||||
} else {
|
||||
linphone_chat_room_delete_remote_composing_refresh_timer(cr);
|
||||
}
|
||||
|
||||
cr->remote_is_composing = state;
|
||||
if (cr->lc->vtable.is_composing_received != NULL)
|
||||
cr->lc->vtable.is_composing_received(cr->lc, cr);
|
||||
}
|
||||
}
|
||||
|
||||
static void linphone_chat_room_notify_is_composing(LinphoneChatRoom *cr, const char *text) {
|
||||
xmlparsing_context_t *xml_ctx = linphone_xmlparsing_context_new();
|
||||
xmlSetGenericErrorFunc(xml_ctx, linphone_xmlparsing_genericxml_error);
|
||||
xml_ctx->doc = xmlReadDoc((const unsigned char*)text, 0, NULL, 0);
|
||||
if (xml_ctx->doc != NULL) {
|
||||
process_im_is_composing_notification(cr, xml_ctx);
|
||||
} else {
|
||||
ms_warning("Wrongly formatted presence XML: %s", xml_ctx->errorBuffer);
|
||||
}
|
||||
linphone_xmlparsing_context_destroy(xml_ctx);
|
||||
}
|
||||
|
||||
void linphone_core_is_composing_received(LinphoneCore *lc, SalOp *op, const SalIsComposing *is_composing) {
|
||||
LinphoneChatRoom *cr = NULL;
|
||||
LinphoneAddress *addr = linphone_address_new(is_composing->from);
|
||||
linphone_address_clean(addr);
|
||||
cr = linphone_core_get_chat_room(lc, addr);
|
||||
if (cr != NULL) {
|
||||
linphone_chat_room_notify_is_composing(cr, is_composing->text);
|
||||
}
|
||||
linphone_address_destroy(addr);
|
||||
}
|
||||
|
||||
bool_t linphone_chat_room_is_remote_composing(const LinphoneChatRoom *cr) {
|
||||
return (cr->remote_is_composing == LinphoneIsComposingActive) ? TRUE : FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns back pointer to LinphoneCore object.
|
||||
**/
|
||||
|
|
@ -319,6 +441,128 @@ void linphone_chat_room_send_message2(LinphoneChatRoom *cr, LinphoneChatMessage*
|
|||
_linphone_chat_room_send_message(cr, msg);
|
||||
}
|
||||
|
||||
static char * linphone_chat_room_create_is_composing_xml(LinphoneChatRoom *cr) {
|
||||
xmlBufferPtr buf;
|
||||
xmlTextWriterPtr writer;
|
||||
int err;
|
||||
char *content = NULL;
|
||||
|
||||
buf = xmlBufferCreate();
|
||||
if (buf == NULL) {
|
||||
ms_error("Error creating the XML buffer");
|
||||
return content;
|
||||
}
|
||||
writer = xmlNewTextWriterMemory(buf, 0);
|
||||
if (writer == NULL) {
|
||||
ms_error("Error creating the XML writer");
|
||||
return content;
|
||||
}
|
||||
|
||||
err = xmlTextWriterStartDocument(writer, "1.0", "UTF-8", NULL);
|
||||
if (err >= 0) {
|
||||
err = xmlTextWriterStartElementNS(writer, NULL, (const xmlChar *)"isComposing", (const xmlChar *)"urn:ietf:params:xml:ns:im-iscomposing");
|
||||
}
|
||||
if (err >= 0) {
|
||||
err = xmlTextWriterWriteAttributeNS(writer, (const xmlChar *)"xmlns", (const xmlChar *)"xsi",
|
||||
NULL, (const xmlChar *)"http://www.w3.org/2001/XMLSchema-instance");
|
||||
}
|
||||
if (err >= 0) {
|
||||
err = xmlTextWriterWriteAttributeNS(writer, (const xmlChar *)"xsi", (const xmlChar *)"schemaLocation",
|
||||
NULL, (const xmlChar *)"urn:ietf:params:xml:ns:im-composing iscomposing.xsd");
|
||||
}
|
||||
if (err >= 0) {
|
||||
err = xmlTextWriterWriteElement(writer, (const xmlChar *)"state",
|
||||
(cr->is_composing == LinphoneIsComposingActive) ? (const xmlChar *)"active" : (const xmlChar *)"idle");
|
||||
}
|
||||
if ((err >= 0) && (cr->is_composing == LinphoneIsComposingActive)) {
|
||||
char refresh_str[4] = { 0 };
|
||||
snprintf(refresh_str, sizeof(refresh_str), "%u", COMPOSING_DEFAULT_REFRESH_TIMEOUT);
|
||||
err = xmlTextWriterWriteElement(writer, (const xmlChar *)"refresh", (const xmlChar *)refresh_str);
|
||||
}
|
||||
if (err >= 0) {
|
||||
/* Close the "isComposing" element. */
|
||||
err = xmlTextWriterEndElement(writer);
|
||||
}
|
||||
if (err >= 0) {
|
||||
err = xmlTextWriterEndDocument(writer);
|
||||
}
|
||||
if (err > 0) {
|
||||
/* xmlTextWriterEndDocument returns the size of the content. */
|
||||
content = ms_strdup((char *)buf->content);
|
||||
}
|
||||
xmlFreeTextWriter(writer);
|
||||
xmlBufferFree(buf);
|
||||
return content;
|
||||
}
|
||||
|
||||
static void linphone_chat_room_send_is_composing_notification(LinphoneChatRoom *cr) {
|
||||
SalOp *op = NULL;
|
||||
LinphoneCall *call;
|
||||
const char *identity = NULL;
|
||||
char *content = NULL;
|
||||
|
||||
if (lp_config_get_int(cr->lc->config, "sip", "chat_use_call_dialogs", 0)) {
|
||||
if ((call = linphone_core_get_call_by_remote_address(cr->lc, cr->peer)) != NULL) {
|
||||
if (call->state == LinphoneCallConnected ||
|
||||
call->state == LinphoneCallStreamsRunning ||
|
||||
call->state == LinphoneCallPaused ||
|
||||
call->state == LinphoneCallPausing ||
|
||||
call->state == LinphoneCallPausedByRemote) {
|
||||
ms_message("send SIP message through the existing call.");
|
||||
op = call->op;
|
||||
identity = linphone_core_find_best_identity(cr->lc, linphone_call_get_remote_address(call));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (op == NULL) {
|
||||
LinphoneProxyConfig *proxy = linphone_core_lookup_known_proxy(cr->lc, cr->peer_url);
|
||||
if (proxy)
|
||||
identity = linphone_proxy_config_get_identity(proxy);
|
||||
else
|
||||
identity = linphone_core_get_primary_contact(cr->lc);
|
||||
/*sending out of calls*/
|
||||
op = sal_op_new(cr->lc->sal);
|
||||
linphone_configure_op(cr->lc, op, cr->peer_url, NULL, lp_config_get_int(cr->lc->config, "sip", "chat_msg_with_contact", 0));
|
||||
}
|
||||
content = linphone_chat_room_create_is_composing_xml(cr);
|
||||
if (content != NULL) {
|
||||
sal_message_send(op, identity, cr->peer, "application/im-iscomposing+xml", content);
|
||||
ms_free(content);
|
||||
}
|
||||
}
|
||||
|
||||
static int linphone_chat_room_stop_composing(void *data, unsigned int revents) {
|
||||
LinphoneChatRoom *cr = (LinphoneChatRoom *)data;
|
||||
cr->is_composing = LinphoneIsComposingIdle;
|
||||
linphone_chat_room_send_is_composing_notification(cr);
|
||||
linphone_chat_room_delete_composing_refresh_timer(cr);
|
||||
belle_sip_object_unref(cr->composing_idle_timer);
|
||||
cr->composing_idle_timer = NULL;
|
||||
return BELLE_SIP_STOP;
|
||||
}
|
||||
|
||||
static int linphone_chat_room_refresh_composing(void *data, unsigned int revents) {
|
||||
LinphoneChatRoom *cr = (LinphoneChatRoom *)data;
|
||||
linphone_chat_room_send_is_composing_notification(cr);
|
||||
return BELLE_SIP_CONTINUE;
|
||||
}
|
||||
|
||||
void linphone_chat_room_compose(LinphoneChatRoom *cr) {
|
||||
if (cr->is_composing == LinphoneIsComposingIdle) {
|
||||
cr->is_composing = LinphoneIsComposingActive;
|
||||
linphone_chat_room_send_is_composing_notification(cr);
|
||||
if (!cr->composing_refresh_timer) {
|
||||
cr->composing_refresh_timer = sal_create_timer(cr->lc->sal, linphone_chat_room_refresh_composing, cr, COMPOSING_DEFAULT_REFRESH_TIMEOUT * 1000, "composing refresh timeout");
|
||||
} else {
|
||||
belle_sip_source_set_timeout(cr->composing_refresh_timer, COMPOSING_DEFAULT_REFRESH_TIMEOUT * 1000);
|
||||
}
|
||||
if (!cr->composing_idle_timer) {
|
||||
cr->composing_idle_timer = sal_create_timer(cr->lc->sal, linphone_chat_room_stop_composing, cr, COMPOSING_DEFAULT_IDLE_TIMEOUT * 1000, "composing idle timeout");
|
||||
}
|
||||
}
|
||||
belle_sip_source_set_timeout(cr->composing_idle_timer, COMPOSING_DEFAULT_IDLE_TIMEOUT * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a #LinphoneChatMessageState as a string.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -977,6 +977,20 @@ LINPHONE_PUBLIC MSList *linphone_chat_room_get_history(LinphoneChatRoom *cr,int
|
|||
LINPHONE_PUBLIC void linphone_chat_room_mark_as_read(LinphoneChatRoom *cr);
|
||||
LINPHONE_PUBLIC void linphone_chat_room_delete_message(LinphoneChatRoom *cr, LinphoneChatMessage *msg);
|
||||
LINPHONE_PUBLIC void linphone_chat_room_delete_history(LinphoneChatRoom *cr);
|
||||
|
||||
/**
|
||||
* Notify the destination of the chat message being composed that the user is typing a new message.
|
||||
* @param[in] cr The #LinphoneChatRoom object corresponding to the conversation for which a new message is being typed.
|
||||
*/
|
||||
LINPHONE_PUBLIC void linphone_chat_room_compose(LinphoneChatRoom *cr);
|
||||
|
||||
/**
|
||||
* Tells whether the remote is currently composing a message.
|
||||
* @param[in] cr The "LinphoneChatRoom object corresponding to the conversation.
|
||||
* @return TRUE if the remote is currently composing a message, FALSE otherwise.
|
||||
*/
|
||||
LINPHONE_PUBLIC bool_t linphone_chat_room_is_remote_composing(const LinphoneChatRoom *cr);
|
||||
|
||||
LINPHONE_PUBLIC int linphone_chat_room_get_unread_messages_count(LinphoneChatRoom *cr);
|
||||
LINPHONE_PUBLIC LinphoneCore* linphone_chat_room_get_lc(LinphoneChatRoom *cr);
|
||||
LINPHONE_PUBLIC void linphone_chat_room_set_user_data(LinphoneChatRoom *cr, void * ud);
|
||||
|
|
@ -1130,6 +1144,14 @@ typedef void (*LinphoneCoreTextMessageReceivedCb)(LinphoneCore *lc, LinphoneChat
|
|||
*/
|
||||
typedef void (*LinphoneCoreMessageReceivedCb)(LinphoneCore *lc, LinphoneChatRoom *room, LinphoneChatMessage *message);
|
||||
|
||||
/**
|
||||
* Is composing notification callback prototype.
|
||||
*
|
||||
* @param[in] lc #LinphoneCore object
|
||||
* @param[in] room #LinphoneChatRoom involved in the conversation.
|
||||
*/
|
||||
typedef void (*LinphoneCoreIsComposingReceivedCb)(LinphoneCore *lc, LinphoneChatRoom *room);
|
||||
|
||||
/**
|
||||
* Callback for being notified of DTMFs received.
|
||||
* @param lc the linphone core
|
||||
|
|
@ -1179,6 +1201,7 @@ typedef struct _LinphoneCoreVTable{
|
|||
LinphoneCoreAuthInfoRequestedCb auth_info_requested; /**< Ask the application some authentication information */
|
||||
LinphoneCoreCallLogUpdatedCb call_log_updated; /**< Notifies that call log list has been updated */
|
||||
LinphoneCoreMessageReceivedCb message_received; /** a message is received, can be text or external body*/
|
||||
LinphoneCoreIsComposingReceivedCb is_composing_received; /**< An is-composing notification has been received */
|
||||
LinphoneCoreDtmfReceivedCb dtmf_received; /**< A dtmf has been received received */
|
||||
LinphoneCoreReferReceivedCb refer_received; /**< An out of call refer was received */
|
||||
LinphoneCoreCallEncryptionChangedCb call_encryption_changed; /**<Notifies on change in the encryption of call streams */
|
||||
|
|
|
|||
|
|
@ -22,15 +22,6 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
|||
#include "lpconfig.h"
|
||||
#include "linphonepresence.h"
|
||||
|
||||
#include <libxml/xmlreader.h>
|
||||
#include <libxml/xmlwriter.h>
|
||||
#include <libxml/xpath.h>
|
||||
#include <libxml/xpathInternals.h>
|
||||
|
||||
|
||||
#define XMLPARSING_BUFFER_LEN 2048
|
||||
#define MAX_XPATH_LENGTH 256
|
||||
|
||||
|
||||
|
||||
extern const char *__policy_enum_to_str(LinphoneSubscribePolicy pol);
|
||||
|
|
@ -83,13 +74,6 @@ struct _LinphonePresenceModel {
|
|||
MSList *notes; /**< A list of _LinphonePresenceNote structures. */
|
||||
};
|
||||
|
||||
typedef struct _xmlparsing_context {
|
||||
xmlDoc *doc;
|
||||
xmlXPathContextPtr xpath_ctx;
|
||||
char errorBuffer[XMLPARSING_BUFFER_LEN];
|
||||
char warningBuffer[XMLPARSING_BUFFER_LEN];
|
||||
} xmlparsing_context_t;
|
||||
|
||||
|
||||
static const char *person_prefix = "/pidf:presence/dm:person";
|
||||
|
||||
|
|
@ -98,38 +82,6 @@ static const char *person_prefix = "/pidf:presence/dm:person";
|
|||
* PRIVATE FUNCTIONS *
|
||||
****************************************************************************/
|
||||
|
||||
static xmlparsing_context_t * xmlparsing_context_new() {
|
||||
xmlparsing_context_t *xmlCtx = (xmlparsing_context_t *)malloc(sizeof(xmlparsing_context_t));
|
||||
if (xmlCtx != NULL) {
|
||||
xmlCtx->doc = NULL;
|
||||
xmlCtx->xpath_ctx = NULL;
|
||||
xmlCtx->errorBuffer[0] = '\0';
|
||||
xmlCtx->warningBuffer[0] = '\0';
|
||||
}
|
||||
return xmlCtx;
|
||||
}
|
||||
|
||||
static void xmlparsing_context_destroy(xmlparsing_context_t *ctx) {
|
||||
if (ctx->doc != NULL) {
|
||||
xmlFreeDoc(ctx->doc);
|
||||
ctx->doc = NULL;
|
||||
}
|
||||
if (ctx->xpath_ctx != NULL) {
|
||||
xmlXPathFreeContext(ctx->xpath_ctx);
|
||||
ctx->xpath_ctx = NULL;
|
||||
}
|
||||
free(ctx);
|
||||
}
|
||||
|
||||
static void xmlparsing_genericxml_error(void *ctx, const char *fmt, ...) {
|
||||
xmlparsing_context_t *xmlCtx = (xmlparsing_context_t *)ctx;
|
||||
int sl = strlen(xmlCtx->errorBuffer);
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
vsnprintf(xmlCtx->errorBuffer + sl, XMLPARSING_BUFFER_LEN - sl, fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
static char presence_id_valid_characters[] = "0123456789abcdefghijklmnopqrstuvwxyz";
|
||||
|
||||
static char * generate_presence_id(void) {
|
||||
|
|
@ -1183,45 +1135,6 @@ void * linphone_presence_note_get_user_data(LinphonePresenceNote *note) {
|
|||
* XML PRESENCE INTERNAL HANDLING *
|
||||
****************************************************************************/
|
||||
|
||||
static int create_xml_xpath_context(xmlparsing_context_t *xml_ctx) {
|
||||
if (xml_ctx->xpath_ctx != NULL) {
|
||||
xmlXPathFreeContext(xml_ctx->xpath_ctx);
|
||||
}
|
||||
xml_ctx->xpath_ctx = xmlXPathNewContext(xml_ctx->doc);
|
||||
if (xml_ctx->xpath_ctx == NULL) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static char * get_xml_text_content(xmlparsing_context_t *xml_ctx, const char *xpath_expression) {
|
||||
xmlXPathObjectPtr xpath_obj;
|
||||
xmlChar *text = NULL;
|
||||
int i;
|
||||
|
||||
xpath_obj = xmlXPathEvalExpression((const xmlChar *)xpath_expression, xml_ctx->xpath_ctx);
|
||||
if (xpath_obj != NULL) {
|
||||
if (xpath_obj->nodesetval != NULL) {
|
||||
xmlNodeSetPtr nodes = xpath_obj->nodesetval;
|
||||
for (i = 0; i < nodes->nodeNr; i++) {
|
||||
xmlNodePtr node = nodes->nodeTab[i];
|
||||
if (node->children != NULL) {
|
||||
text = xmlNodeListGetString(xml_ctx->doc, node->children, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
xmlXPathFreeObject(xpath_obj);
|
||||
}
|
||||
|
||||
return (char *)text;
|
||||
}
|
||||
|
||||
static void free_xml_text_content(const char *text) {
|
||||
xmlFree((xmlChar *)text);
|
||||
}
|
||||
|
||||
static xmlXPathObjectPtr get_xml_xpath_object_for_node_list(xmlparsing_context_t *xml_ctx, const char *xpath_expression) {
|
||||
return xmlXPathEvalExpression((const xmlChar *)xpath_expression, xml_ctx->xpath_ctx);
|
||||
}
|
||||
|
||||
static const char *service_prefix = "/pidf:presence/pidf:tuple";
|
||||
|
||||
static int process_pidf_xml_presence_service_notes(xmlparsing_context_t *xml_ctx, LinphonePresenceService *service, unsigned int service_idx) {
|
||||
|
|
@ -1233,19 +1146,19 @@ static int process_pidf_xml_presence_service_notes(xmlparsing_context_t *xml_ctx
|
|||
int i;
|
||||
|
||||
snprintf(xpath_str, sizeof(xpath_str), "%s[%i]/pidf:note", service_prefix, service_idx);
|
||||
note_object = get_xml_xpath_object_for_node_list(xml_ctx, xpath_str);
|
||||
note_object = linphone_get_xml_xpath_object_for_node_list(xml_ctx, xpath_str);
|
||||
if ((note_object != NULL) && (note_object->nodesetval != NULL)) {
|
||||
for (i = 1; i <= note_object->nodesetval->nodeNr; i++) {
|
||||
snprintf(xpath_str, sizeof(xpath_str), "%s[%i]/pidf:note[%i]", service_prefix, service_idx, i);
|
||||
note_str = get_xml_text_content(xml_ctx, xpath_str);
|
||||
note_str = linphone_get_xml_text_content(xml_ctx, xpath_str);
|
||||
if (note_str == NULL) continue;
|
||||
snprintf(xpath_str, sizeof(xpath_str), "%s[%i]/pidf:note[%i]/@xml:lang", service_prefix, service_idx, i);
|
||||
lang = get_xml_text_content(xml_ctx, xpath_str);
|
||||
lang = linphone_get_xml_text_content(xml_ctx, xpath_str);
|
||||
|
||||
note = linphone_presence_note_new(note_str, lang);
|
||||
presence_service_add_note(service, note);
|
||||
if (lang != NULL) free_xml_text_content(lang);
|
||||
free_xml_text_content(note_str);
|
||||
if (lang != NULL) linphone_free_xml_text_content(lang);
|
||||
linphone_free_xml_text_content(note_str);
|
||||
}
|
||||
}
|
||||
if (note_object != NULL) xmlXPathFreeObject(note_object);
|
||||
|
|
@ -1264,11 +1177,11 @@ static int process_pidf_xml_presence_services(xmlparsing_context_t *xml_ctx, Lin
|
|||
LinphonePresenceBasicStatus basic_status;
|
||||
int i;
|
||||
|
||||
service_object = get_xml_xpath_object_for_node_list(xml_ctx, service_prefix);
|
||||
service_object = linphone_get_xml_xpath_object_for_node_list(xml_ctx, service_prefix);
|
||||
if ((service_object != NULL) && (service_object->nodesetval != NULL)) {
|
||||
for (i = 1; i <= service_object->nodesetval->nodeNr; i++) {
|
||||
snprintf(xpath_str, sizeof(xpath_str), "%s[%i]/pidf:status/pidf:basic", service_prefix, i);
|
||||
basic_status_str = get_xml_text_content(xml_ctx, xpath_str);
|
||||
basic_status_str = linphone_get_xml_text_content(xml_ctx, xpath_str);
|
||||
if (basic_status_str == NULL)
|
||||
continue;
|
||||
|
||||
|
|
@ -1278,33 +1191,33 @@ static int process_pidf_xml_presence_services(xmlparsing_context_t *xml_ctx, Lin
|
|||
basic_status = LinphonePresenceBasicStatusClosed;
|
||||
} else {
|
||||
/* Invalid value for basic status. */
|
||||
free_xml_text_content(basic_status_str);
|
||||
linphone_free_xml_text_content(basic_status_str);
|
||||
return -1;
|
||||
}
|
||||
|
||||
snprintf(xpath_str, sizeof(xpath_str), "%s[%i]/pidf:timestamp", service_prefix, i);
|
||||
timestamp_str = get_xml_text_content(xml_ctx, xpath_str);
|
||||
timestamp_str = linphone_get_xml_text_content(xml_ctx, xpath_str);
|
||||
|
||||
snprintf(xpath_str, sizeof(xpath_str), "%s[%i]/pidf:contact", service_prefix, i);
|
||||
contact_str = get_xml_text_content(xml_ctx, xpath_str);
|
||||
contact_str = linphone_get_xml_text_content(xml_ctx, xpath_str);
|
||||
|
||||
snprintf(xpath_str, sizeof(xpath_str), "%s[%i]/@id", service_prefix, i);
|
||||
service_id_str = get_xml_text_content(xml_ctx, xpath_str);
|
||||
service_id_str = linphone_get_xml_text_content(xml_ctx, xpath_str);
|
||||
service = presence_service_new(service_id_str, basic_status);
|
||||
if (service != NULL) {
|
||||
if (timestamp_str != NULL) {
|
||||
presence_service_set_timestamp(service, parse_timestamp(timestamp_str));
|
||||
free_xml_text_content(timestamp_str);
|
||||
linphone_free_xml_text_content(timestamp_str);
|
||||
}
|
||||
if (contact_str != NULL) {
|
||||
linphone_presence_service_set_contact(service, contact_str);
|
||||
free_xml_text_content(contact_str);
|
||||
linphone_free_xml_text_content(contact_str);
|
||||
}
|
||||
process_pidf_xml_presence_service_notes(xml_ctx, service, i);
|
||||
linphone_presence_model_add_service(model, service);
|
||||
}
|
||||
free_xml_text_content(basic_status_str);
|
||||
if (service_id_str != NULL) free_xml_text_content(service_id_str);
|
||||
linphone_free_xml_text_content(basic_status_str);
|
||||
if (service_id_str != NULL) linphone_free_xml_text_content(service_id_str);
|
||||
}
|
||||
}
|
||||
if (service_object != NULL) xmlXPathFreeObject(service_object);
|
||||
|
|
@ -1333,11 +1246,11 @@ static int process_pidf_xml_presence_person_activities(xmlparsing_context_t *xml
|
|||
int err = 0;
|
||||
|
||||
snprintf(xpath_str, sizeof(xpath_str), "%s[%i]/rpid:activities", person_prefix, person_idx);
|
||||
activities_nodes_object = get_xml_xpath_object_for_node_list(xml_ctx, xpath_str);
|
||||
activities_nodes_object = linphone_get_xml_xpath_object_for_node_list(xml_ctx, xpath_str);
|
||||
if ((activities_nodes_object != NULL) && (activities_nodes_object->nodesetval != NULL)) {
|
||||
for (i = 1; i <= activities_nodes_object->nodesetval->nodeNr; i++) {
|
||||
snprintf(xpath_str, sizeof(xpath_str), "%s[%i]/rpid:activities[%i]/rpid:*", person_prefix, person_idx, i);
|
||||
activities_object = get_xml_xpath_object_for_node_list(xml_ctx, xpath_str);
|
||||
activities_object = linphone_get_xml_xpath_object_for_node_list(xml_ctx, xpath_str);
|
||||
if ((activities_object != NULL) && (activities_object->nodesetval != NULL)) {
|
||||
for (j = 0; j < activities_object->nodesetval->nodeNr; j++) {
|
||||
activity_node = activities_object->nodesetval->nodeTab[j];
|
||||
|
|
@ -1345,14 +1258,14 @@ static int process_pidf_xml_presence_person_activities(xmlparsing_context_t *xml
|
|||
LinphonePresenceActivityType acttype;
|
||||
description = (const char *)xmlNodeGetContent(activity_node);
|
||||
if ((description != NULL) && (description[0] == '\0')) {
|
||||
free_xml_text_content(description);
|
||||
linphone_free_xml_text_content(description);
|
||||
description = NULL;
|
||||
}
|
||||
err = activity_name_to_presence_activity_type((const char *)activity_node->name, &acttype);
|
||||
if (err < 0) break;
|
||||
activity = linphone_presence_activity_new(acttype, description);
|
||||
linphone_presence_person_add_activity(person, activity);
|
||||
if (description != NULL) free_xml_text_content(description);
|
||||
if (description != NULL) linphone_free_xml_text_content(description);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1374,37 +1287,37 @@ static int process_pidf_xml_presence_person_notes(xmlparsing_context_t *xml_ctx,
|
|||
int i;
|
||||
|
||||
snprintf(xpath_str, sizeof(xpath_str), "%s[%i]/rpid:activities/rpid:note", person_prefix, person_idx);
|
||||
note_object = get_xml_xpath_object_for_node_list(xml_ctx, xpath_str);
|
||||
note_object = linphone_get_xml_xpath_object_for_node_list(xml_ctx, xpath_str);
|
||||
if ((note_object != NULL) && (note_object->nodesetval != NULL)) {
|
||||
for (i = 1; i <= note_object->nodesetval->nodeNr; i++) {
|
||||
snprintf(xpath_str, sizeof(xpath_str), "%s[%i]/rpid:activities/rpid:note[%i]", person_prefix, person_idx, i);
|
||||
note_str = get_xml_text_content(xml_ctx, xpath_str);
|
||||
note_str = linphone_get_xml_text_content(xml_ctx, xpath_str);
|
||||
if (note_str == NULL) continue;
|
||||
snprintf(xpath_str, sizeof(xpath_str), "%s[%i]/rpid:activities/rpid:note[%i]/@xml:lang", person_prefix, person_idx, i);
|
||||
lang = get_xml_text_content(xml_ctx, xpath_str);
|
||||
lang = linphone_get_xml_text_content(xml_ctx, xpath_str);
|
||||
|
||||
note = linphone_presence_note_new(note_str, lang);
|
||||
presence_person_add_activities_note(person, note);
|
||||
if (lang != NULL) free_xml_text_content(lang);
|
||||
free_xml_text_content(note_str);
|
||||
if (lang != NULL) linphone_free_xml_text_content(lang);
|
||||
linphone_free_xml_text_content(note_str);
|
||||
}
|
||||
}
|
||||
if (note_object != NULL) xmlXPathFreeObject(note_object);
|
||||
|
||||
snprintf(xpath_str, sizeof(xpath_str), "%s[%i]/dm:note", person_prefix, person_idx);
|
||||
note_object = get_xml_xpath_object_for_node_list(xml_ctx, xpath_str);
|
||||
note_object = linphone_get_xml_xpath_object_for_node_list(xml_ctx, xpath_str);
|
||||
if ((note_object != NULL) && (note_object->nodesetval != NULL)) {
|
||||
for (i = 1; i <= note_object->nodesetval->nodeNr; i++) {
|
||||
snprintf(xpath_str, sizeof(xpath_str), "%s[%i]/dm:note[%i]", person_prefix, person_idx, i);
|
||||
note_str = get_xml_text_content(xml_ctx, xpath_str);
|
||||
note_str = linphone_get_xml_text_content(xml_ctx, xpath_str);
|
||||
if (note_str == NULL) continue;
|
||||
snprintf(xpath_str, sizeof(xpath_str), "%s[%i]/dm:note[%i]/@xml:lang", person_prefix, person_idx, i);
|
||||
lang = get_xml_text_content(xml_ctx, xpath_str);
|
||||
lang = linphone_get_xml_text_content(xml_ctx, xpath_str);
|
||||
|
||||
note = linphone_presence_note_new(note_str, lang);
|
||||
presence_person_add_note(person, note);
|
||||
if (lang != NULL) free_xml_text_content(lang);
|
||||
free_xml_text_content(note_str);
|
||||
if (lang != NULL) linphone_free_xml_text_content(lang);
|
||||
linphone_free_xml_text_content(note_str);
|
||||
}
|
||||
}
|
||||
if (note_object != NULL) xmlXPathFreeObject(note_object);
|
||||
|
|
@ -1422,13 +1335,13 @@ static int process_pidf_xml_presence_persons(xmlparsing_context_t *xml_ctx, Linp
|
|||
int i;
|
||||
int err = 0;
|
||||
|
||||
person_object = get_xml_xpath_object_for_node_list(xml_ctx, person_prefix);
|
||||
person_object = linphone_get_xml_xpath_object_for_node_list(xml_ctx, person_prefix);
|
||||
if ((person_object != NULL) && (person_object->nodesetval != NULL)) {
|
||||
for (i = 1; i <= person_object->nodesetval->nodeNr; i++) {
|
||||
snprintf(xpath_str, sizeof(xpath_str), "%s[%i]/@id", person_prefix, i);
|
||||
person_id_str = get_xml_text_content(xml_ctx, xpath_str);
|
||||
person_id_str = linphone_get_xml_text_content(xml_ctx, xpath_str);
|
||||
snprintf(xpath_str, sizeof(xpath_str), "%s[%i]/pidf:timestamp", person_prefix, i);
|
||||
person_timestamp_str = get_xml_text_content(xml_ctx, xpath_str);
|
||||
person_timestamp_str = linphone_get_xml_text_content(xml_ctx, xpath_str);
|
||||
if (person_timestamp_str == NULL)
|
||||
timestamp = time(NULL);
|
||||
else
|
||||
|
|
@ -1446,8 +1359,8 @@ static int process_pidf_xml_presence_persons(xmlparsing_context_t *xml_ctx, Linp
|
|||
break;
|
||||
}
|
||||
}
|
||||
if (person_id_str != NULL) free_xml_text_content(person_id_str);
|
||||
if (person_timestamp_str != NULL) free_xml_text_content(person_timestamp_str);
|
||||
if (person_id_str != NULL) linphone_free_xml_text_content(person_id_str);
|
||||
if (person_timestamp_str != NULL) linphone_free_xml_text_content(person_timestamp_str);
|
||||
}
|
||||
}
|
||||
if (person_object != NULL) xmlXPathFreeObject(person_object);
|
||||
|
|
@ -1467,19 +1380,19 @@ static int process_pidf_xml_presence_notes(xmlparsing_context_t *xml_ctx, Linpho
|
|||
const char *lang;
|
||||
int i;
|
||||
|
||||
note_object = get_xml_xpath_object_for_node_list(xml_ctx, "/pidf:presence/pidf:note");
|
||||
note_object = linphone_get_xml_xpath_object_for_node_list(xml_ctx, "/pidf:presence/pidf:note");
|
||||
if ((note_object != NULL) && (note_object->nodesetval != NULL)) {
|
||||
for (i = 1; i <= note_object->nodesetval->nodeNr; i++) {
|
||||
snprintf(xpath_str, sizeof(xpath_str), "/pidf:presence/pidf:note[%i]", i);
|
||||
note_str = get_xml_text_content(xml_ctx, xpath_str);
|
||||
note_str = linphone_get_xml_text_content(xml_ctx, xpath_str);
|
||||
if (note_str == NULL) continue;
|
||||
snprintf(xpath_str, sizeof(xpath_str), "/pidf:presence/pidf:note[%i]/@xml:lang", i);
|
||||
lang = get_xml_text_content(xml_ctx, xpath_str);
|
||||
lang = linphone_get_xml_text_content(xml_ctx, xpath_str);
|
||||
|
||||
note = linphone_presence_note_new(note_str, lang);
|
||||
presence_model_add_note(model, note);
|
||||
if (lang != NULL) free_xml_text_content(lang);
|
||||
free_xml_text_content(note_str);
|
||||
if (lang != NULL) linphone_free_xml_text_content(lang);
|
||||
linphone_free_xml_text_content(note_str);
|
||||
}
|
||||
}
|
||||
if (note_object != NULL) xmlXPathFreeObject(note_object);
|
||||
|
|
@ -1491,7 +1404,7 @@ static LinphonePresenceModel * process_pidf_xml_presence_notification(xmlparsing
|
|||
LinphonePresenceModel *model = NULL;
|
||||
int err;
|
||||
|
||||
if (create_xml_xpath_context(xml_ctx) < 0)
|
||||
if (linphone_create_xml_xpath_context(xml_ctx) < 0)
|
||||
return NULL;
|
||||
|
||||
model = linphone_presence_model_new();
|
||||
|
|
@ -1606,15 +1519,15 @@ void linphone_notify_parse_presence(SalOp *op, const char *content_type, const c
|
|||
}
|
||||
|
||||
if (strcmp(content_subtype, "pidf+xml") == 0) {
|
||||
xml_ctx = xmlparsing_context_new();
|
||||
xmlSetGenericErrorFunc(xml_ctx, xmlparsing_genericxml_error);
|
||||
xml_ctx = linphone_xmlparsing_context_new();
|
||||
xmlSetGenericErrorFunc(xml_ctx, linphone_xmlparsing_genericxml_error);
|
||||
xml_ctx->doc = xmlReadDoc((const unsigned char*)body, 0, NULL, 0);
|
||||
if (xml_ctx->doc != NULL) {
|
||||
model = process_pidf_xml_presence_notification(xml_ctx);
|
||||
} else {
|
||||
ms_warning("Wrongly formatted presence XML: %s", xml_ctx->errorBuffer);
|
||||
}
|
||||
xmlparsing_context_destroy(xml_ctx);
|
||||
linphone_xmlparsing_context_destroy(xml_ctx);
|
||||
} else {
|
||||
ms_error("Unknown content type '%s/%s' for presence", content_type, content_subtype);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -338,6 +338,7 @@ void linphone_proxy_config_write_to_config_file(struct _LpConfig* config,Linphon
|
|||
int linphone_proxy_config_normalize_number(LinphoneProxyConfig *cfg, const char *username, char *result, size_t result_len);
|
||||
|
||||
void linphone_core_message_received(LinphoneCore *lc, SalOp *op, const SalMessage *msg);
|
||||
void linphone_core_is_composing_received(LinphoneCore *lc, SalOp *op, const SalIsComposing *is_composing);
|
||||
|
||||
void linphone_core_play_tone(LinphoneCore *lc);
|
||||
|
||||
|
|
@ -426,12 +427,22 @@ struct _LinphoneAuthInfo
|
|||
bool_t works;
|
||||
};
|
||||
|
||||
typedef enum _LinphoneIsComposingState {
|
||||
LinphoneIsComposingIdle,
|
||||
LinphoneIsComposingActive
|
||||
} LinphoneIsComposingState;
|
||||
|
||||
struct _LinphoneChatRoom{
|
||||
struct _LinphoneCore *lc;
|
||||
char *peer;
|
||||
LinphoneAddress *peer_url;
|
||||
void * user_data;
|
||||
MSList *messages_hist;
|
||||
LinphoneIsComposingState remote_is_composing;
|
||||
LinphoneIsComposingState is_composing;
|
||||
belle_sip_source_t *remote_composing_refresh_timer;
|
||||
belle_sip_source_t *composing_idle_timer;
|
||||
belle_sip_source_t *composing_refresh_timer;
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -788,6 +799,35 @@ LinphoneSubscriptionState linphone_subscription_state_from_sal(SalSubscribeStatu
|
|||
const LinphoneContent *linphone_content_from_sal_body(LinphoneContent *obj, const SalBody *ref);
|
||||
void linphone_core_invalidate_friend_subscriptions(LinphoneCore *lc);
|
||||
|
||||
|
||||
/*****************************************************************************
|
||||
* XML UTILITY FUNCTIONS *
|
||||
****************************************************************************/
|
||||
|
||||
#include <libxml/xmlreader.h>
|
||||
#include <libxml/xmlwriter.h>
|
||||
#include <libxml/xpath.h>
|
||||
#include <libxml/xpathInternals.h>
|
||||
|
||||
#define XMLPARSING_BUFFER_LEN 2048
|
||||
#define MAX_XPATH_LENGTH 256
|
||||
|
||||
typedef struct _xmlparsing_context {
|
||||
xmlDoc *doc;
|
||||
xmlXPathContextPtr xpath_ctx;
|
||||
char errorBuffer[XMLPARSING_BUFFER_LEN];
|
||||
char warningBuffer[XMLPARSING_BUFFER_LEN];
|
||||
} xmlparsing_context_t;
|
||||
|
||||
xmlparsing_context_t * linphone_xmlparsing_context_new(void);
|
||||
void linphone_xmlparsing_context_destroy(xmlparsing_context_t *ctx);
|
||||
void linphone_xmlparsing_genericxml_error(void *ctx, const char *fmt, ...);
|
||||
int linphone_create_xml_xpath_context(xmlparsing_context_t *xml_ctx);
|
||||
char * linphone_get_xml_text_content(xmlparsing_context_t *xml_ctx, const char *xpath_expression);
|
||||
void linphone_free_xml_text_content(const char *text);
|
||||
xmlXPathObjectPtr linphone_get_xml_xpath_object_for_node_list(xmlparsing_context_t *xml_ctx, const char *xpath_expression);
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
98
coreapi/xml.c
Normal file
98
coreapi/xml.c
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
linphone
|
||||
Copyright (C) 2010-2013 Belledonne Communications SARL
|
||||
|
||||
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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
|
||||
#include "private.h"
|
||||
|
||||
#include <libxml/xmlreader.h>
|
||||
#include <libxml/xmlwriter.h>
|
||||
#include <libxml/xpath.h>
|
||||
#include <libxml/xpathInternals.h>
|
||||
|
||||
|
||||
xmlparsing_context_t * linphone_xmlparsing_context_new(void) {
|
||||
xmlparsing_context_t *xmlCtx = (xmlparsing_context_t *)malloc(sizeof(xmlparsing_context_t));
|
||||
if (xmlCtx != NULL) {
|
||||
xmlCtx->doc = NULL;
|
||||
xmlCtx->xpath_ctx = NULL;
|
||||
xmlCtx->errorBuffer[0] = '\0';
|
||||
xmlCtx->warningBuffer[0] = '\0';
|
||||
}
|
||||
return xmlCtx;
|
||||
}
|
||||
|
||||
void linphone_xmlparsing_context_destroy(xmlparsing_context_t *ctx) {
|
||||
if (ctx->doc != NULL) {
|
||||
xmlFreeDoc(ctx->doc);
|
||||
ctx->doc = NULL;
|
||||
}
|
||||
if (ctx->xpath_ctx != NULL) {
|
||||
xmlXPathFreeContext(ctx->xpath_ctx);
|
||||
ctx->xpath_ctx = NULL;
|
||||
}
|
||||
free(ctx);
|
||||
}
|
||||
|
||||
void linphone_xmlparsing_genericxml_error(void *ctx, const char *fmt, ...) {
|
||||
xmlparsing_context_t *xmlCtx = (xmlparsing_context_t *)ctx;
|
||||
int sl = strlen(xmlCtx->errorBuffer);
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
vsnprintf(xmlCtx->errorBuffer + sl, XMLPARSING_BUFFER_LEN - sl, fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
int linphone_create_xml_xpath_context(xmlparsing_context_t *xml_ctx) {
|
||||
if (xml_ctx->xpath_ctx != NULL) {
|
||||
xmlXPathFreeContext(xml_ctx->xpath_ctx);
|
||||
}
|
||||
xml_ctx->xpath_ctx = xmlXPathNewContext(xml_ctx->doc);
|
||||
if (xml_ctx->xpath_ctx == NULL) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
char * linphone_get_xml_text_content(xmlparsing_context_t *xml_ctx, const char *xpath_expression) {
|
||||
xmlXPathObjectPtr xpath_obj;
|
||||
xmlChar *text = NULL;
|
||||
int i;
|
||||
|
||||
xpath_obj = xmlXPathEvalExpression((const xmlChar *)xpath_expression, xml_ctx->xpath_ctx);
|
||||
if (xpath_obj != NULL) {
|
||||
if (xpath_obj->nodesetval != NULL) {
|
||||
xmlNodeSetPtr nodes = xpath_obj->nodesetval;
|
||||
for (i = 0; i < nodes->nodeNr; i++) {
|
||||
xmlNodePtr node = nodes->nodeTab[i];
|
||||
if (node->children != NULL) {
|
||||
text = xmlNodeListGetString(xml_ctx->doc, node->children, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
xmlXPathFreeObject(xpath_obj);
|
||||
}
|
||||
|
||||
return (char *)text;
|
||||
}
|
||||
|
||||
void linphone_free_xml_text_content(const char *text) {
|
||||
xmlFree((xmlChar *)text);
|
||||
}
|
||||
|
||||
xmlXPathObjectPtr linphone_get_xml_xpath_object_for_node_list(xmlparsing_context_t *xml_ctx, const char *xpath_expression) {
|
||||
return xmlXPathEvalExpression((const xmlChar *)xpath_expression, xml_ctx->xpath_ctx);
|
||||
}
|
||||
21
gtk/chat.c
21
gtk/chat.c
|
|
@ -279,6 +279,16 @@ static void on_chat_state_changed(LinphoneChatMessage *msg, LinphoneChatMessageS
|
|||
update_chat_state_message(state,msg);
|
||||
}
|
||||
|
||||
void linphone_gtk_compose_text(void) {
|
||||
GtkWidget *main_window=linphone_gtk_get_main_window();
|
||||
GtkWidget *friendlist=linphone_gtk_get_widget(main_window,"contact_list");
|
||||
GtkWidget *w=(GtkWidget*)g_object_get_data(G_OBJECT(friendlist),"chatview");
|
||||
LinphoneChatRoom *cr=g_object_get_data(G_OBJECT(w),"cr");
|
||||
if (cr) {
|
||||
linphone_chat_room_compose(cr);
|
||||
}
|
||||
}
|
||||
|
||||
void linphone_gtk_send_text(){
|
||||
GtkWidget *main_window=linphone_gtk_get_main_window();
|
||||
GtkWidget *friendlist=linphone_gtk_get_widget(main_window,"contact_list");
|
||||
|
|
@ -293,7 +303,11 @@ void linphone_gtk_send_text(){
|
|||
linphone_chat_room_send_message2(cr,msg,on_chat_state_changed,NULL);
|
||||
linphone_gtk_push_text(w,linphone_chat_message_get_from(msg),
|
||||
TRUE,cr,msg,FALSE);
|
||||
|
||||
// Disconnect and reconnect the "changed" signal to prevent triggering it when clearing the text entry.
|
||||
g_signal_handlers_disconnect_by_func(G_OBJECT(entry),(GCallback)linphone_gtk_compose_text,NULL);
|
||||
gtk_entry_set_text(GTK_ENTRY(entry),"");
|
||||
g_signal_connect_swapped(G_OBJECT(entry),"changed",(GCallback)linphone_gtk_compose_text,NULL);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -410,6 +424,7 @@ GtkWidget* linphone_gtk_init_chatroom(LinphoneChatRoom *cr, const LinphoneAddres
|
|||
g_signal_connect_swapped(G_OBJECT(button),"clicked",(GCallback)linphone_gtk_send_text,NULL);
|
||||
entry = linphone_gtk_get_widget(chat_view,"text_entry");
|
||||
g_signal_connect_swapped(G_OBJECT(entry),"activate",(GCallback)linphone_gtk_send_text,NULL);
|
||||
g_signal_connect_swapped(G_OBJECT(entry),"changed",(GCallback)linphone_gtk_compose_text,NULL);
|
||||
g_signal_connect(G_OBJECT(notebook),"switch_page",(GCallback)linphone_gtk_notebook_tab_select,NULL);
|
||||
ms_free(with_str);
|
||||
return chat_view;
|
||||
|
|
@ -417,7 +432,7 @@ GtkWidget* linphone_gtk_init_chatroom(LinphoneChatRoom *cr, const LinphoneAddres
|
|||
|
||||
LinphoneChatRoom * linphone_gtk_create_chatroom(const LinphoneAddress *with){
|
||||
char *tmp=linphone_address_as_string(with);
|
||||
LinphoneChatRoom *cr=linphone_core_create_chat_room(linphone_gtk_get_core(),tmp);
|
||||
LinphoneChatRoom *cr=linphone_core_get_or_create_chat_room(linphone_gtk_get_core(),tmp);
|
||||
ms_free(tmp);
|
||||
return cr;
|
||||
}
|
||||
|
|
@ -516,3 +531,7 @@ void linphone_gtk_text_received ( LinphoneCore *lc, LinphoneChatRoom *room,
|
|||
linphone_gtk_show_friends();
|
||||
|
||||
}
|
||||
|
||||
void linphone_gtk_is_composing_received(LinphoneCore *lc, LinphoneChatRoom *room) {
|
||||
linphone_gtk_friend_list_update_chat_picture();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,6 +87,18 @@ static GdkPixbuf *create_chat_picture(){
|
|||
return pixbuf;
|
||||
}
|
||||
|
||||
static GdkPixbuf *create_composing_unread_msg(){
|
||||
GdkPixbuf *pixbuf;
|
||||
pixbuf = create_pixbuf("composing_active_chat.png");
|
||||
return pixbuf;
|
||||
}
|
||||
|
||||
static GdkPixbuf *create_composing_chat_picture(){
|
||||
GdkPixbuf *pixbuf;
|
||||
pixbuf = create_pixbuf("composing_chat.png");
|
||||
return pixbuf;
|
||||
}
|
||||
|
||||
/*
|
||||
void linphone_gtk_set_friend_status(GtkWidget *friendlist , LinphoneFriend * fid, const gchar *url, const gchar *status, const gchar *img){
|
||||
GtkTreeIter iter;
|
||||
|
|
@ -227,15 +239,23 @@ void linphone_gtk_friend_list_update_chat_picture(){
|
|||
GtkWidget *friendlist=linphone_gtk_get_widget(w,"contact_list");
|
||||
GtkTreeModel *model=gtk_tree_view_get_model(GTK_TREE_VIEW(friendlist));
|
||||
LinphoneChatRoom *cr=NULL;
|
||||
bool_t is_composing;
|
||||
int nbmsg=0;
|
||||
if (gtk_tree_model_get_iter_first(model,&iter)) {
|
||||
do{
|
||||
gtk_tree_model_get (model, &iter,FRIEND_CHATROOM , &cr, -1);
|
||||
nbmsg=linphone_chat_room_get_unread_messages_count(cr);
|
||||
is_composing=linphone_chat_room_is_remote_composing(cr);
|
||||
if(nbmsg != 0){
|
||||
gtk_list_store_set(GTK_LIST_STORE(model),&iter,FRIEND_CHAT,create_unread_msg(),-1);
|
||||
if (is_composing == TRUE)
|
||||
gtk_list_store_set(GTK_LIST_STORE(model),&iter,FRIEND_CHAT,create_composing_unread_msg(),-1);
|
||||
else
|
||||
gtk_list_store_set(GTK_LIST_STORE(model),&iter,FRIEND_CHAT,create_unread_msg(),-1);
|
||||
} else {
|
||||
gtk_list_store_set(GTK_LIST_STORE(model),&iter,FRIEND_CHAT,create_chat_picture(),-1);
|
||||
if (is_composing == TRUE)
|
||||
gtk_list_store_set(GTK_LIST_STORE(model),&iter,FRIEND_CHAT,create_composing_chat_picture(),-1);
|
||||
else
|
||||
gtk_list_store_set(GTK_LIST_STORE(model),&iter,FRIEND_CHAT,create_chat_picture(),-1);
|
||||
}
|
||||
}while(gtk_tree_model_iter_next(model,&iter));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ void linphone_gtk_send_text();
|
|||
GtkWidget * linphone_gtk_init_chatroom(LinphoneChatRoom *cr, const LinphoneAddress *with);
|
||||
LinphoneChatRoom * linphone_gtk_create_chatroom(const LinphoneAddress *with);
|
||||
void linphone_gtk_text_received(LinphoneCore *lc, LinphoneChatRoom *room, LinphoneChatMessage *msg);
|
||||
void linphone_gtk_is_composing_received(LinphoneCore *lc, LinphoneChatRoom *room);
|
||||
|
||||
void linphone_gtk_friend_list_update_chat_picture();
|
||||
void linphone_gtk_friend_list_set_chat_conversation(const LinphoneAddress *la);
|
||||
|
|
|
|||
|
|
@ -266,6 +266,7 @@ static void linphone_gtk_init_liblinphone(const char *config_file,
|
|||
vtable.call_log_updated=linphone_gtk_call_log_updated;
|
||||
//vtable.text_received=linphone_gtk_text_received;
|
||||
vtable.message_received=linphone_gtk_text_received;
|
||||
vtable.is_composing_received=linphone_gtk_is_composing_received;
|
||||
vtable.refer_received=linphone_gtk_refer_received;
|
||||
vtable.buddy_info_updated=linphone_gtk_buddy_info_updated;
|
||||
vtable.call_encryption_changed=linphone_gtk_call_encryption_changed;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
|||
|
||||
#include "mediastreamer2/mscommon.h"
|
||||
#include "ortp/ortp_srtp.h"
|
||||
#include "belle-sip/belle-sip.h"
|
||||
|
||||
#ifndef LINPHONE_PUBLIC
|
||||
#define LINPHONE_PUBLIC MS2_PUBLIC
|
||||
|
|
@ -224,6 +225,11 @@ typedef struct SalMessage{
|
|||
time_t time;
|
||||
}SalMessage;
|
||||
|
||||
typedef struct SalIsComposing {
|
||||
const char *from;
|
||||
const char *text;
|
||||
} SalIsComposing;
|
||||
|
||||
#define SAL_MEDIA_DESCRIPTION_MAX_MESSAGE_ATTRIBUTES 5
|
||||
|
||||
SalMediaDescription *sal_media_description_new();
|
||||
|
|
@ -390,6 +396,7 @@ typedef void (*SalOnDtmfReceived)(SalOp *op, char dtmf);
|
|||
typedef void (*SalOnRefer)(Sal *sal, SalOp *op, const char *referto);
|
||||
typedef void (*SalOnTextReceived)(SalOp *op, const SalMessage *msg);
|
||||
typedef void (*SalOnTextDeliveryUpdate)(SalOp *op, SalTextDeliveryStatus status);
|
||||
typedef void (*SalOnIsComposingReceived)(SalOp *op, const SalIsComposing *is_composing);
|
||||
typedef void (*SalOnNotifyRefer)(SalOp *op, SalReferStatus state);
|
||||
typedef void (*SalOnSubscribeResponse)(SalOp *op, SalSubscribeStatus status, SalError error, SalReason reason);
|
||||
typedef void (*SalOnNotify)(SalOp *op, SalSubscribeStatus status, const char *event, const SalBody *body);
|
||||
|
|
@ -425,6 +432,7 @@ typedef struct SalCallbacks{
|
|||
SalOnRefer refer_received;
|
||||
SalOnTextReceived text_received;
|
||||
SalOnTextDeliveryUpdate text_delivery_update;
|
||||
SalOnIsComposingReceived is_composing_received;
|
||||
SalOnNotifyRefer notify_refer;
|
||||
SalOnSubscribeReceived subscribe_received;
|
||||
SalOnSubscribeClosed subscribe_closed;
|
||||
|
|
@ -690,6 +698,8 @@ LINPHONE_PUBLIC bool_t sal_dns_srv_enabled(const Sal *sal);
|
|||
LINPHONE_PUBLIC void sal_set_dns_user_hosts_file(Sal *sal, const char *hosts_file);
|
||||
LINPHONE_PUBLIC const char *sal_get_dns_user_hosts_file(const Sal *sal);
|
||||
unsigned char * sal_get_random_bytes(unsigned char *ret, size_t size);
|
||||
belle_sip_source_t * sal_create_timer(Sal *sal, belle_sip_source_func_t func, void *data, unsigned int timeout_value_ms, const char* timer_name);
|
||||
void sal_cancel_timer(Sal *sal, belle_sip_source_t *timer);
|
||||
|
||||
int sal_body_has_type(const SalBody *body, const char *type, const char *subtype);
|
||||
/*this function parses a document with key=value pairs separated by new lines, and extracts the value for a given key*/
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ status-orange.png \
|
|||
status-red.png \
|
||||
status-offline.png \
|
||||
call.png \
|
||||
chat.png active_chat.png\
|
||||
chat.png active_chat.png composing_chat.png composing_active_chat.png\
|
||||
chat_message_inprogress.png chat_message_delivered.png chat_message_not_delivered.png\
|
||||
contact-orange.png dialer-orange.png history-orange.png\
|
||||
startcall-green.png startcall-small.png stopcall-red.png stopcall-small.png addcall-green.png linphone.icns \
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
BIN
pixmaps/composing_active_chat.png
Normal file
BIN
pixmaps/composing_active_chat.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
BIN
pixmaps/composing_chat.png
Normal file
BIN
pixmaps/composing_chat.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
|
|
@ -23,11 +23,11 @@ liblinphone_tester_SOURCES= liblinphone_tester.c liblinphone_tester.h\
|
|||
|
||||
AM_CPPFLAGS=-I$(top_srcdir)/include -I$(top_srcdir)/coreapi
|
||||
|
||||
LDADD=$(top_builddir)/coreapi/liblinphone.la $(BELLESIP_LIBS)
|
||||
LDADD=$(top_builddir)/coreapi/liblinphone.la $(BELLESIP_LIBS) $(LIBXML2_LIBS)
|
||||
|
||||
AM_LDFLAGS=$(CUNIT_LIBS)
|
||||
|
||||
AM_CFLAGS=$(STRICT_OPTIONS) -DIN_LINPHONE $(ORTP_CFLAGS) $(MEDIASTREAMER_CFLAGS) $(CUNIT_CFLAGS) $(BELLESIP_CFLAGS)
|
||||
AM_CFLAGS=$(STRICT_OPTIONS) -DIN_LINPHONE $(ORTP_CFLAGS) $(MEDIASTREAMER_CFLAGS) $(CUNIT_CFLAGS) $(BELLESIP_CFLAGS) $(LIBXML2_CFLAGS)
|
||||
|
||||
test: liblinphone_tester
|
||||
./liblinphone_tester --config $(abs_srcdir)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue