libpurple/purpleconversation.c

Tue, 15 Oct 2024 00:47:42 -0500

author
Elliott Sales de Andrade <quantum.analyst@gmail.com>
date
Tue, 15 Oct 2024 00:47:42 -0500
changeset 43011
ce3144e2bc33
parent 42999
5a506dee26d2
child 43017
09661a988eab
permissions
-rw-r--r--

Port prefs to AdwSwitchRow

Now that we depend on Adwaita 1.4, we can flip the switch on using these (pun intended).

This also simplifies some extra tracking we needed to do for activations and focus, since the Adwaita widgets do that for us.

Testing Done:
Opened prefs, confirmed all the switches were there, and toggled them all without any warnings.

Also used the mnemonics to toggle the switches from the keyboard.

Reviewed at https://reviews.imfreedom.org/r/3582/

/*
 * Purple - Internet Messaging Library
 * Copyright (C) Pidgin Developers <devel@pidgin.im>
 *
 * Purple is the legal property of its developers, whose names are too numerous
 * to list here. Please refer to the COPYRIGHT file distributed with this
 * source distribution.
 *
 * This library 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 library 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 library; if not, see <https://www.gnu.org/licenses/>.
 */

#include <glib/gi18n-lib.h>

#include "purpleconversation.h"

#include "debug.h"
#include "purpleconversationmanager.h"
#include "purpleconversationmember.h"
#include "purpleenums.h"
#include "purplehistorymanager.h"
#include "purplemarkup.h"
#include "purpleprotocolconversation.h"
#include "purpletags.h"
#include "request.h"
#include "util.h"

struct _PurpleConversation {
	GObject parent;

	char *id;
	PurpleConversationType type;
	PurpleAccount *account;

	PurpleAvatar *avatar;
	char *name;
	char *alias;
	char *title;
	gboolean title_generated;

	gboolean age_restricted;
	char *description;
	char *topic;
	PurpleContactInfo *topic_author;
	GDateTime *topic_updated;
	char *user_nickname;
	gboolean favorite;
	GDateTime *created_on;
	PurpleContactInfo *creator;
	gboolean online;
	gboolean federated;
	PurpleTags *tags;

	PurpleConversationMembers *members;

	GListStore *messages;
	gboolean needs_attention;

	PurpleTypingState typing_state;
	guint typing_state_source;
	GDateTime *last_typing;

	gboolean logging;
	gboolean drafting;
};

enum {
	PROP_0,
	PROP_ID,
	PROP_GLOBAL_ID,
	PROP_TYPE,
	PROP_ACCOUNT,
	PROP_AVATAR,
	PROP_NAME,
	PROP_ALIAS,
	PROP_TITLE,
	PROP_TITLE_FOR_DISPLAY,
	PROP_TITLE_GENERATED,
	PROP_AGE_RESTRICTED,
	PROP_DESCRIPTION,
	PROP_TOPIC,
	PROP_TOPIC_AUTHOR,
	PROP_TOPIC_UPDATED,
	PROP_USER_NICKNAME,
	PROP_FAVORITE,
	PROP_CREATED_ON,
	PROP_CREATOR,
	PROP_ONLINE,
	PROP_FEDERATED,
	PROP_TAGS,
	PROP_MEMBERS,
	PROP_MESSAGES,
	PROP_NEEDS_ATTENTION,
	PROP_TYPING_STATE,
	PROP_LOGGING,
	PROP_DRAFTING,
	N_PROPERTIES,
};
static GParamSpec *properties[N_PROPERTIES] = {NULL, };

enum {
	SIG_PRESENT,
	N_SIGNALS,
};
static guint signals[N_SIGNALS] = {0, };

G_DEFINE_FINAL_TYPE(PurpleConversation, purple_conversation, G_TYPE_OBJECT)

static void purple_conversation_account_connected_cb(GObject *obj,
                                                     GParamSpec *pspec,
                                                     gpointer data);

/**************************************************************************
 * Helpers
 **************************************************************************/
static void
purple_conversation_set_title_generated(PurpleConversation *conversation,
                                        gboolean title_generated)
{
	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	/* If conversation isn't a dm or group dm, and title_generated is being set
	 * to %TRUE exit immediately because generating the title is only allowed
	 * on DMs and GroupDMs.
	 */
	if(conversation->type != PURPLE_CONVERSATION_TYPE_DM &&
	   conversation->type != PURPLE_CONVERSATION_TYPE_GROUP_DM &&
	   title_generated)
	{
		return;
	}

	if(conversation->title_generated != title_generated) {
		GObject *obj = G_OBJECT(conversation);

		conversation->title_generated = title_generated;

		g_object_freeze_notify(obj);

		if(conversation->title_generated) {
			purple_conversation_generate_title(conversation);
		}

		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_TITLE_GENERATED]);

		g_object_thaw_notify(obj);
	}
}

static void
purple_conversation_set_id(PurpleConversation *conversation, const char *id) {
	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	if(g_set_str(&conversation->id, id)) {
		GObject *obj = G_OBJECT(conversation);

		g_object_freeze_notify(obj);
		g_object_notify_by_pspec(obj, properties[PROP_ID]);
		g_object_notify_by_pspec(obj, properties[PROP_GLOBAL_ID]);
		g_object_thaw_notify(obj);
	}
}

static void
purple_conversation_set_account(PurpleConversation *conversation,
                                PurpleAccount *account)
{
	PurpleConversationMember *member = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	/* Remove the account from the conversation if it's a member. */
	if(PURPLE_IS_ACCOUNT(conversation->account)) {
		if(PURPLE_IS_CONVERSATION_MEMBER(member)) {
			PurpleContactInfo *info = NULL;

			info = purple_account_get_contact_info(conversation->account);
			purple_conversation_members_remove_member(conversation->members,
			                                          info, FALSE, NULL);
		}
	}

	if(g_set_object(&conversation->account, account)) {
		GObject *obj = NULL;

		if(PURPLE_IS_ACCOUNT(conversation->account)) {
			PurpleContactInfo *info = NULL;
			PurpleConversationMember *member = NULL;
			const char *tag_value = NULL;

			tag_value = purple_account_get_id(conversation->account);
			purple_tags_add_with_value(conversation->tags, "account-id",
			                           tag_value);

			tag_value = purple_account_get_protocol_id(conversation->account);
			purple_tags_add_with_value(conversation->tags, "protocol-id",
			                           tag_value);

			info = purple_account_get_contact_info(account);
			member = purple_conversation_members_add_member(conversation->members,
			                                                info, FALSE, NULL);

			g_object_bind_property(conversation, "typing-state",
			                       member, "typing-state",
			                       G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE);

			g_signal_connect_object(account, "notify::connected",
			                        G_CALLBACK(purple_conversation_account_connected_cb),
			                        conversation, G_CONNECT_DEFAULT);
		}

		obj = G_OBJECT(conversation);
		g_object_freeze_notify(obj);
		g_object_notify_by_pspec(obj, properties[PROP_ACCOUNT]);
		g_object_notify_by_pspec(obj, properties[PROP_GLOBAL_ID]);
		g_object_thaw_notify(obj);
	}
}

static void
purple_conversation_set_conversation_type(PurpleConversation *conversation,
                                          PurpleConversationType type)
{
	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	if(type != conversation->type) {
		const char *tag_value = NULL;

		conversation->type = type;

		switch(conversation->type) {
		case PURPLE_CONVERSATION_TYPE_DM:
			tag_value = "dm";
			break;
		case PURPLE_CONVERSATION_TYPE_GROUP_DM:
			tag_value = "group-dm";
			break;
		case PURPLE_CONVERSATION_TYPE_CHANNEL:
			tag_value = "channel";
			break;
		case PURPLE_CONVERSATION_TYPE_THREAD:
			tag_value = "thread";
			break;
		case PURPLE_CONVERSATION_TYPE_UNSET:
		default:
			tag_value = NULL;
		}

		purple_tags_add_with_value(conversation->tags, "type", tag_value);

		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_TYPE]);
	}
}

static void
purple_conversation_set_federated(PurpleConversation *conversation,
                                  gboolean federated)
{
	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	if(conversation->federated != federated) {
		conversation->federated = federated;

		if(federated) {
			purple_tags_add(conversation->tags, "federated");
		}

		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_FEDERATED]);
	}
}

static void
purple_conversation_send_message_async_cb(GObject *source,
                                          GAsyncResult *result,
                                          gpointer data)
{
	PurpleMessage *message = NULL;
	PurpleProtocolConversation *protocol = NULL;
	GError *error = NULL;
	GTask *task = data;
	gboolean success = FALSE;

	/* task and result share a cancellable, so we just need to clear task to
	 * make sure its callback gets called.
	 */
	if(g_task_return_error_if_cancelled(G_TASK(task))) {
		g_clear_object(&task);

		return;
	}

	protocol = PURPLE_PROTOCOL_CONVERSATION(source);
	message = g_task_get_task_data(G_TASK(task));

	success = purple_protocol_conversation_send_message_finish(protocol,
	                                                           result, &error);

	if(!success) {
		if(error == NULL) {
			error = g_error_new(PURPLE_CONVERSATION_DOMAIN, 0,
			                    "unknown error");
		}

		purple_message_set_error(message, error);
		g_task_return_error(task, error);

		g_clear_error(&error);
	} else {
		/* If the protocol didn't set delivered, set it now. */
		if(!purple_message_get_delivered(message)) {
			purple_message_set_delivered(message, TRUE);
		}

		g_task_return_boolean(task, TRUE);
	}

	g_clear_object(&task);
}

/**************************************************************************
 * Callbacks
 **************************************************************************/
static void
purple_conversation_members_item_changed_cb(G_GNUC_UNUSED GListModel *model,
                                            G_GNUC_UNUSED guint position,
                                            G_GNUC_UNUSED guint removed,
                                            G_GNUC_UNUSED guint added,
                                            gpointer data)
{
	PurpleConversation *conversation = data;

	if(purple_conversation_get_title_generated(conversation)) {
		purple_conversation_generate_title(conversation);
	}
}

static void
purple_conversation_members_member_added_cb(G_GNUC_UNUSED PurpleConversationMembers *members,
                                            PurpleConversationMember *member,
                                            gboolean announce,
                                            const char *join_message,
                                            gpointer data)
{
	PurpleConversation *conversation = data;
	PurpleMessage *message = NULL;
	char *contents = NULL;

	if(!announce) {
		return;
	}

	if(purple_strempty(join_message)) {
		contents = g_strdup_printf(_("%s has joined the conversation"),
		                           purple_conversation_member_get_name_for_display(member));
	} else {
		contents = g_strdup_printf(_("%s has joined the conversation: %s"),
		                           purple_conversation_member_get_name_for_display(member),
		                           join_message);
	}

	message = g_object_new(
		PURPLE_TYPE_MESSAGE,
		"contents", contents,
		"event", TRUE,
		NULL);

	g_free(contents);

	purple_conversation_write_message(conversation, message);
}

static void
purple_conversation_members_member_removed_cb(G_GNUC_UNUSED PurpleConversationMembers *members,
                                              PurpleConversationMember *member,
                                              gboolean announce,
                                              const char *part_message,
                                              gpointer data)
{
	PurpleConversation *conversation = data;
	PurpleMessage *message = NULL;
	char *contents = NULL;

	if(!announce) {
		return;
	}

	if(purple_strempty(part_message)) {
		contents = g_strdup_printf(_("%s has left the conversation"),
		                           purple_conversation_member_get_name_for_display(member));
	} else {
		contents = g_strdup_printf(_("%s has left the conversation: %s"),
		                           purple_conversation_member_get_name_for_display(member),
		                           part_message);
	}

	message = g_object_new(
		PURPLE_TYPE_MESSAGE,
		"contents", contents,
		"event", TRUE,
		NULL);

	g_free(contents);

	purple_conversation_write_message(conversation, message);
}

static void
purple_conversation_account_connected_cb(GObject *obj,
                                         G_GNUC_UNUSED GParamSpec *pspec,
                                         gpointer data)
{
	PurpleConversation *conversation = data;
	gboolean connected = purple_account_is_connected(PURPLE_ACCOUNT(obj));

	if(conversation->federated) {
		/* If the account changed to connected and the conversation is
		 * federated we do nothing. But if the account went offline, we can
		 * safely set the conversation to offline.
		 */
		if(!connected) {
			purple_conversation_set_online(conversation, FALSE);
		}
	} else {
		purple_conversation_set_online(conversation, connected);
	}
}

/*
 * purple_conversation_typing_state_typing_cb: (skip)
 * @data: The conversation instance.
 *
 * If this callback manages to get called, it means the user has stopped typing
 * and we need to change the typing state of the conversation to paused.
 *
 * There's some specific ordering we have to worry about because
 * purple_conversation_set_typing_state will attempt to remove the source that
 * called us even though we're going to exit cleanly after we call that
 * function.
 *
 * To avoid this, we just set the typing_state_source to 0 which will make
 * purple_conversation_set_typing_state not try to cancel the source.
 */
static void
purple_conversation_typing_state_typing_cb(gpointer data) {
	PurpleConversation *conversation = data;

	conversation->typing_state_source = 0;

	purple_conversation_set_typing_state(conversation,
	                                     PURPLE_TYPING_STATE_PAUSED);
}

/*
 * purple_conversation_typing_state_paused_cb: (skip)
 * @data: The conversation instance.
 *
 * If this callback manages to get called, it means the user has stopped typing
 * some time ago, and we need to set the state to NONE.
 *
 * There's some specific ordering we have to worry about because
 * purple_conversation_set_typing_state will attempt to remove the source that
 * called us even though we're going to exit cleanly after we call that
 * function.
 *
 * To avoid this, we just set the typing_state_source to 0 which will make
 * purple_conversation_set_typing_state not try to cancel the source.
 */
static void
purple_conversation_typing_state_paused_cb(gpointer data) {
	PurpleConversation *conversation = data;

	conversation->typing_state_source = 0;

	purple_conversation_set_typing_state(conversation,
	                                     PURPLE_TYPING_STATE_NONE);
}

/**************************************************************************
 * GObject Implementation
 **************************************************************************/
static void
purple_conversation_set_property(GObject *obj, guint param_id,
                                 const GValue *value, GParamSpec *pspec)
{
	PurpleConversation *conversation = PURPLE_CONVERSATION(obj);

	switch (param_id) {
	case PROP_ID:
		purple_conversation_set_id(conversation, g_value_get_string(value));
		break;
	case PROP_TYPE:
		purple_conversation_set_conversation_type(conversation,
		                                          g_value_get_enum(value));
		break;
	case PROP_ACCOUNT:
		purple_conversation_set_account(conversation,
		                                g_value_get_object(value));
		break;
	case PROP_AVATAR:
		purple_conversation_set_avatar(conversation,
		                               g_value_get_object(value));
		break;
	case PROP_NAME:
		purple_conversation_set_name(conversation, g_value_get_string(value));
		break;
	case PROP_ALIAS:
		purple_conversation_set_alias(conversation, g_value_get_string(value));
		break;
	case PROP_TITLE:
		purple_conversation_set_title(conversation, g_value_get_string(value));
		break;
	case PROP_AGE_RESTRICTED:
		purple_conversation_set_age_restricted(conversation,
		                                       g_value_get_boolean(value));
		break;
	case PROP_DESCRIPTION:
		purple_conversation_set_description(conversation,
		                                    g_value_get_string(value));
		break;
	case PROP_TOPIC:
		purple_conversation_set_topic(conversation, g_value_get_string(value));
		break;
	case PROP_TOPIC_AUTHOR:
		purple_conversation_set_topic_author(conversation,
		                                     g_value_get_object(value));
		break;
	case PROP_TOPIC_UPDATED:
		purple_conversation_set_topic_updated(conversation,
		                                      g_value_get_boxed(value));
		break;
	case PROP_USER_NICKNAME:
		purple_conversation_set_user_nickname(conversation,
		                                      g_value_get_string(value));
		break;
	case PROP_FAVORITE:
		purple_conversation_set_favorite(conversation,
		                                 g_value_get_boolean(value));
		break;
	case PROP_CREATED_ON:
		purple_conversation_set_created_on(conversation,
		                                   g_value_get_boxed(value));
		break;
	case PROP_CREATOR:
		purple_conversation_set_creator(conversation,
		                                g_value_get_object(value));
		break;
	case PROP_ONLINE:
		purple_conversation_set_online(conversation,
		                               g_value_get_boolean(value));
		break;
	case PROP_FEDERATED:
		purple_conversation_set_federated(conversation,
		                                  g_value_get_boolean(value));
		break;
	case PROP_NEEDS_ATTENTION:
		purple_conversation_set_needs_attention(conversation,
		                                        g_value_get_boolean(value));
		break;
	case PROP_TYPING_STATE:
		purple_conversation_set_typing_state(conversation,
		                                     g_value_get_enum(value));
		break;
	case PROP_LOGGING:
		purple_conversation_set_logging(conversation,
		                                g_value_get_boolean(value));
		break;
	case PROP_DRAFTING:
		purple_conversation_set_drafting(conversation,
		                                 g_value_get_boolean(value));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
purple_conversation_get_property(GObject *obj, guint param_id, GValue *value,
                                 GParamSpec *pspec)
{
	PurpleConversation *conversation = PURPLE_CONVERSATION(obj);

	switch(param_id) {
	case PROP_ID:
		g_value_set_string(value, purple_conversation_get_id(conversation));
		break;
	case PROP_GLOBAL_ID:
		g_value_take_string(value,
		                    purple_conversation_get_global_id(conversation));
		break;
	case PROP_TYPE:
		g_value_set_enum(value,
		                 purple_conversation_get_conversation_type(conversation));
		break;
	case PROP_ACCOUNT:
		g_value_set_object(value,
		                   purple_conversation_get_account(conversation));
		break;
	case PROP_AVATAR:
		g_value_set_object(value,
		                   purple_conversation_get_avatar(conversation));
		break;
	case PROP_NAME:
		g_value_set_string(value, purple_conversation_get_name(conversation));
		break;
	case PROP_ALIAS:
		g_value_set_string(value, purple_conversation_get_alias(conversation));
		break;
	case PROP_TITLE:
		g_value_set_string(value, purple_conversation_get_title(conversation));
		break;
	case PROP_TITLE_FOR_DISPLAY:
		g_value_set_string(value,
		                   purple_conversation_get_title_for_display(conversation));
		break;
	case PROP_TITLE_GENERATED:
		g_value_set_boolean(value,
		                    purple_conversation_get_title_generated(conversation));
		break;
	case PROP_AGE_RESTRICTED:
		g_value_set_boolean(value,
		                    purple_conversation_get_age_restricted(conversation));
		break;
	case PROP_DESCRIPTION:
		g_value_set_string(value,
		                   purple_conversation_get_description(conversation));
		break;
	case PROP_TOPIC:
		g_value_set_string(value, purple_conversation_get_topic(conversation));
		break;
	case PROP_TOPIC_AUTHOR:
		g_value_set_object(value,
		                   purple_conversation_get_topic_author(conversation));
		break;
	case PROP_TOPIC_UPDATED:
		g_value_set_boxed(value,
		                  purple_conversation_get_topic_updated(conversation));
		break;
	case PROP_USER_NICKNAME:
		g_value_set_string(value,
		                   purple_conversation_get_user_nickname(conversation));
		break;
	case PROP_FAVORITE:
		g_value_set_boolean(value,
		                    purple_conversation_get_favorite(conversation));
		break;
	case PROP_CREATED_ON:
		g_value_set_boxed(value,
		                  purple_conversation_get_created_on(conversation));
		break;
	case PROP_CREATOR:
		g_value_set_object(value,
		                   purple_conversation_get_creator(conversation));
		break;
	case PROP_ONLINE:
		g_value_set_boolean(value,
		                    purple_conversation_get_online(conversation));
		break;
	case PROP_FEDERATED:
		g_value_set_boolean(value,
		                    purple_conversation_get_federated(conversation));
		break;
	case PROP_TAGS:
		g_value_set_object(value, purple_conversation_get_tags(conversation));
		break;
	case PROP_MEMBERS:
		g_value_set_object(value,
		                   purple_conversation_get_members(conversation));
		break;
	case PROP_MESSAGES:
		g_value_set_object(value,
		                   purple_conversation_get_messages(conversation));
		break;
	case PROP_NEEDS_ATTENTION:
		g_value_set_boolean(value,
		                    purple_conversation_get_needs_attention(conversation));
		break;
	case PROP_TYPING_STATE:
		g_value_set_enum(value,
		                 purple_conversation_get_typing_state(conversation));
		break;
	case PROP_LOGGING:
		g_value_set_boolean(value,
		                    purple_conversation_get_logging(conversation));
		break;
	case PROP_DRAFTING:
		g_value_set_boolean(value,
		                    purple_conversation_get_drafting(conversation));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
purple_conversation_init(PurpleConversation *conversation) {
	conversation->tags = purple_tags_new();

	/* If type provided during construction, the type setter isn't called,
	 * which means this tag doesn't get created. To work around this, we set it
	 * here, which will be overridden by the type setter.
	 */
	purple_tags_add(conversation->tags, "type");

	conversation->messages = g_list_store_new(PURPLE_TYPE_MESSAGE);

	conversation->members = purple_conversation_members_new();
	g_signal_connect_object(conversation->members, "items-changed",
	                        G_CALLBACK(purple_conversation_members_item_changed_cb),
	                        conversation, G_CONNECT_DEFAULT);
	g_signal_connect_object(conversation->members, "member-added",
	                        G_CALLBACK(purple_conversation_members_member_added_cb),
	                        conversation, G_CONNECT_DEFAULT);
	g_signal_connect_object(conversation->members, "member-removed",
	                        G_CALLBACK(purple_conversation_members_member_removed_cb),
	                        conversation, G_CONNECT_DEFAULT);
}

static void
purple_conversation_constructed(GObject *object) {
	PurpleConversation *conversation = PURPLE_CONVERSATION(object);

	G_OBJECT_CLASS(purple_conversation_parent_class)->constructed(object);

	if(purple_strempty(conversation->title)) {
		if(conversation->type == PURPLE_CONVERSATION_TYPE_DM ||
		   conversation->type == PURPLE_CONVERSATION_TYPE_GROUP_DM)
		{
			/* There's no way to add members during construction, so just call
			 * set_title_generated.
			 */
			purple_conversation_set_title_generated(conversation, TRUE);
		}
	}
}

static void
purple_conversation_dispose(GObject *obj) {
	g_object_set_data(obj, "is-finalizing", GINT_TO_POINTER(TRUE));
}

static void
purple_conversation_finalize(GObject *object) {
	PurpleConversation *conversation = PURPLE_CONVERSATION(object);

	purple_request_close_with_handle(conversation);

	g_clear_object(&conversation->account);
	g_clear_pointer(&conversation->id, g_free);
	g_clear_object(&conversation->avatar);
	g_clear_pointer(&conversation->name, g_free);
	g_clear_pointer(&conversation->alias, g_free);
	g_clear_pointer(&conversation->title, g_free);

	g_clear_pointer(&conversation->description, g_free);
	g_clear_pointer(&conversation->topic, g_free);
	g_clear_object(&conversation->topic_author);
	g_clear_pointer(&conversation->topic_updated, g_date_time_unref);
	g_clear_pointer(&conversation->user_nickname, g_free);
	g_clear_pointer(&conversation->created_on, g_date_time_unref);
	g_clear_object(&conversation->creator);
	g_clear_object(&conversation->tags);
	g_clear_object(&conversation->members);
	g_clear_object(&conversation->messages);

	g_clear_handle_id(&conversation->typing_state_source, g_source_remove);
	g_clear_pointer(&conversation->last_typing, g_date_time_unref);

	G_OBJECT_CLASS(purple_conversation_parent_class)->finalize(object);
}

static void
purple_conversation_class_init(PurpleConversationClass *klass) {
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);

	obj_class->constructed = purple_conversation_constructed;
	obj_class->dispose = purple_conversation_dispose;
	obj_class->finalize = purple_conversation_finalize;
	obj_class->get_property = purple_conversation_get_property;
	obj_class->set_property = purple_conversation_set_property;

	/**
	 * PurpleConversation:id:
	 *
	 * An opaque identifier for this conversation. Generally speaking this is
	 * protocol dependent and should only be used as a unique identifier.
	 *
	 * Since: 3.0
	 */
	properties[PROP_ID] = g_param_spec_string(
		"id", "id",
		"The identifier for the conversation.",
		NULL,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:global-id:
	 *
	 * A libpurple global id for the conversation.
	 *
	 * This is an opaque value but it ties the conversation to the libpurple
	 * account it belongs to, which makes it globally unique inside of
	 * libpurple.
	 *
	 * Since: 3.0
	 */
	properties[PROP_GLOBAL_ID] = g_param_spec_string(
		"global-id", NULL, NULL,
		NULL,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:type:
	 *
	 * A type hint for the conversation. This may be useful for protocols, but
	 * libpurple treats all conversations the same.
	 *
	 * Since: 3.0
	 */
	properties[PROP_TYPE] = g_param_spec_enum(
		"type", "type",
		"The type of the conversation.",
		PURPLE_TYPE_CONVERSATION_TYPE,
		PURPLE_CONVERSATION_TYPE_UNSET,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:account:
	 *
	 * The account this conversation belongs to.
	 *
	 * Since: 3.0
	 */
	properties[PROP_ACCOUNT] = g_param_spec_object(
		"account", "Account",
		"The account for the conversation.",
		PURPLE_TYPE_ACCOUNT,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:avatar:
	 *
	 * The [class@Avatar] for the conversation.
	 *
	 * Not all protocols support this and most user interfaces will use the
	 * avatar of the remote contact for direct messages.
	 *
	 * Since: 3.0
	 */
	properties[PROP_AVATAR] = g_param_spec_object(
		"avatar", "avatar",
		"The avatar for this conversation.",
		PURPLE_TYPE_AVATAR,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:name:
	 *
	 * The name of the conversation.
	 *
	 * Since: 3.0
	 */
	properties[PROP_NAME] = g_param_spec_string(
		"name", "Name",
		"The name of the conversation.",
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:alias:
	 *
	 * An alias for the conversation that is local to the libpurple user.
	 *
	 * Since: 3.0
	 */
	properties[PROP_ALIAS] = g_param_spec_string(
		"alias", NULL, NULL,
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:title:
	 *
	 * The title of the conversation.
	 *
	 * Since: 3.0
	 */
	properties[PROP_TITLE] = g_param_spec_string(
		"title", "Title",
		"The title of the conversation.",
		NULL,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:title-for-display:
	 *
	 * The title that should be displayed for the conversation based on which
	 * properties are set.
	 *
	 * If [property@Conversation:alias] is set, that will be returned.
	 *
	 * If alias is not set but [property@Conversation:title] is set, then value
	 * of title will be returned.
	 *
	 * As a fallback, [property@Conversation:id] will be returned if nothing
	 * else is set.
	 *
	 * Since: 3.0
	 */
	properties[PROP_TITLE_FOR_DISPLAY] = g_param_spec_string(
		"title-for-display", NULL, NULL,
		NULL,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:title-generated:
	 *
	 * Whether or not the title of the conversation was generated by
	 * [method@Conversation.generate_title].
	 *
	 * Note: This only works on DMs and GroupDMs.
	 *
	 * If this is %TRUE, [method@Conversation.generate_title] will
	 * automatically be called whenever a member is added or removed, or when
	 * their display name changes.
	 *
	 * Since: 3.0
	 */
	properties[PROP_TITLE_GENERATED] = g_param_spec_boolean(
		"title-generated", "title-generated",
		"Whether or not the current title was generated.",
		FALSE,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:age-restricted:
	 *
	 * Whether or not the conversation is age restricted.
	 *
	 * This is typically set only by a protocol plugin.
	 *
	 * Since: 3.0
	 */
	properties[PROP_AGE_RESTRICTED] = g_param_spec_boolean(
		"age-restricted", "age-restricted",
		"Whether or not the conversation is age restricted.",
		FALSE,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:description:
	 *
	 * Sets the description of the conversation. This field is typically used
	 * to give more information about a conversation than that which would fit
	 * in [property@Conversation:topic].
	 *
	 * This is typically set only by a protocol plugin.
	 *
	 * Since: 3.0
	 */
	properties[PROP_DESCRIPTION] = g_param_spec_string(
		"description", "description",
		"The description for the conversation.",
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:topic:
	 *
	 * The topic of the conversation.
	 *
	 * This is normally controlled by the protocol plugin and often times
	 * requires permission for the user to set.
	 *
	 * Since: 3.0
	 */
	properties[PROP_TOPIC] = g_param_spec_string(
		"topic", "topic",
		"The topic for the conversation.",
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:topic-author:
	 *
	 * Sets the author of the topic for the conversation.
	 *
	 * This should typically only be set by a protocol plugin.
	 *
	 * Since: 3.0
	 */
	properties[PROP_TOPIC_AUTHOR] = g_param_spec_object(
		"topic-author", "topic-author",
		"The author of the topic for the conversation.",
		PURPLE_TYPE_CONTACT_INFO,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:topic-updated:
	 *
	 * Set to the time that the topic was last updated.
	 *
	 * This should typically only be set by a protocol plugin.
	 *
	 * Since: 3.0
	 */
	properties[PROP_TOPIC_UPDATED] = g_param_spec_boxed(
		"topic-updated", "topic-updated",
		"The time when the topic was last updated for the conversation.",
		G_TYPE_DATE_TIME,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:user-nickname:
	 *
	 * The user's nickname in this conversation.
	 *
	 * Some protocols allow the user to use a nickname rather than their normal
	 * contact information when joining a conversation. This field holds that
	 * value.
	 *
	 * Since: 3.0
	 */
	properties[PROP_USER_NICKNAME] = g_param_spec_string(
		"user-nickname", "user-nickname",
		"The nickname for the user in the conversation.",
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:favorite:
	 *
	 * Whether or not the conversation has been marked as favorite by the user.
	 *
	 * Since: 3.0
	 */
	properties[PROP_FAVORITE] = g_param_spec_boolean(
		"favorite", "favorite",
		"Whether or not the conversation is a favorite.",
		FALSE,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:created-on:
	 *
	 * The [struct@GLib.DateTime] when this conversation was created. This can
	 * be %NULL if the value is not known or supported.
	 *
	 * This should typically only be set by a protocol plugin.
	 *
	 * Since: 3.0
	 */
	properties[PROP_CREATED_ON] = g_param_spec_boxed(
		"created-on", "created-on",
		"When the conversation was created.",
		G_TYPE_DATE_TIME,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:creator:
	 *
	 * The [class@ContactInfo] that created the conversation.
	 *
	 * This should typically only be set by a protocol plugin.
	 *
	 * Since: 3.0
	 */
	properties[PROP_CREATOR] = g_param_spec_object(
		"creator", "creator",
		"The contact info of who created the conversation.",
		PURPLE_TYPE_CONTACT_INFO,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:online:
	 *
	 * Whether or not the conversation is able to send and receive messages.
	 *
	 * This is typically tied to whether or not the account that this
	 * conversation belongs is online or not.
	 *
	 * However, if a protocol supports federated conversation, it is possible
	 * for a conversation to be offline if the server it is on is currently
	 * unreachable.
	 *
	 * See also [property@Conversation:federated].
	 *
	 * Since: 3.0
	 */
	properties[PROP_ONLINE] = g_param_spec_boolean(
		"online", "online",
		"Whether or not the conversation can send and receive messages.",
		TRUE,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:federated:
	 *
	 * Whether or this conversation is federated.
	 *
	 * This should only be set by protocols that support federated
	 * conversations.
	 *
	 * When this is %TRUE the [property@Conversation:online] property will not
	 * be automatically set to match the [property@Account:connected] property
	 * of the account that this conversation belongs to. It is the
	 * responsibility of the protocol to manage the online property in this
	 * case.
	 *
	 * Since: 3.0
	 */
	properties[PROP_FEDERATED] = g_param_spec_boolean(
		"federated", "federated",
		"Whether or not this conversation is federated.",
		FALSE,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:tags:
	 *
	 * [class@Tags] for the conversation.
	 *
	 * Since: 3.0
	 */
	properties[PROP_TAGS] = g_param_spec_object(
		"tags", "tags",
		"The tags for the conversation.",
		PURPLE_TYPE_TAGS,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:members:
	 *
	 * The members that are currently in this conversation.
	 *
	 * Since: 3.0
	 */
	properties[PROP_MEMBERS] = g_param_spec_object(
		"members", "members",
		"The members that are currently in this conversation",
		PURPLE_TYPE_CONVERSATION_MEMBERS,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:messages:
	 *
	 * A [iface.Gio.ListModel] of all the messages in this conversation.
	 *
	 * Since: 3.0
	 */
	properties[PROP_MESSAGES] = g_param_spec_object(
		"messages", "messages",
		"All of the messages in this conversation's history.",
		G_TYPE_LIST_MODEL,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:needs-attention:
	 *
	 * Whether or not the conversation needs attention.
	 *
	 * This could be because there are new messages or the user has been
	 * kicked from the room, or something else.
	 *
	 * Since: 3.0
	 */
	properties[PROP_NEEDS_ATTENTION] = g_param_spec_boolean(
		"needs-attention", NULL, NULL,
		FALSE,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:typing-state:
	 *
	 * The [enum@TypingState] of the libpurple user in this conversation.
	 *
	 * When the property changes to `typing`, a timeout will be setup to change
	 * the property to `paused` if the property hasn't been set to `typing`
	 * again before the timeout expires.
	 *
	 * If the above timeout fires, the state will be changed to `paused`, and a
	 * new timeout will be added that will reset the state to `none` if it
	 * expires.
	 *
	 * This means that user interfaces should only ever need to set the state
	 * to typing and should do so whenever the user types anything that could
	 * be part of a message. Things like keyboard navigation and %commands
	 * should not result in this property being changed.
	 *
	 * If the [class@Protocol] that this conversation belongs to implements
	 * [iface@ProtocolConversation] and
	 * [vfunc@ProtocolConversation.send_typing],
	 * [vfunc@ProtocolConversation.send_typing] will be called when this
	 * property is set even if the state hasn't changed.
	 *
	 * Since: 3.0
	 */
	properties[PROP_TYPING_STATE] = g_param_spec_enum(
		"typing-state", NULL, NULL,
		PURPLE_TYPE_TYPING_STATE,
		PURPLE_TYPING_STATE_NONE,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:logging:
	 *
	 * Whether or not this conversation is currently being logged.
	 *
	 * Since: 3.0
	 */
	properties[PROP_LOGGING] = g_param_spec_boolean(
		"logging", NULL, NULL,
		FALSE,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:drafting:
	 *
	 * Whether or not the user has drafted a message for this conversation.
	 *
	 * This will not be set to false after a call to
	 * [method@Conversation.write_message] as anything can call that which
	 * could break the accounting of this property.
	 *
	 * Since: 3.0
	 */
	properties[PROP_DRAFTING] = g_param_spec_boolean(
		"drafting", NULL, NULL,
		FALSE,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);

	/**
	 * PurpleConversation::present:
	 * @conversation: The instance.
	 *
	 * Emitted by [method@Conversation.present] when something wants the
	 * conversation presented to the user.
	 *
	 * Since: 3.0
	 */
	signals[SIG_PRESENT] = g_signal_new_class_handler(
		"present",
		G_OBJECT_CLASS_TYPE(klass),
		G_SIGNAL_RUN_LAST,
		NULL,
		NULL,
		NULL,
		NULL,
		G_TYPE_NONE,
		0);
}

/******************************************************************************
 * Public API
 *****************************************************************************/
gboolean
purple_conversation_is_dm(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);

	return conversation->type == PURPLE_CONVERSATION_TYPE_DM;
}

gboolean
purple_conversation_is_group_dm(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);

	return conversation->type == PURPLE_CONVERSATION_TYPE_GROUP_DM;
}

gboolean
purple_conversation_is_channel(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);

	return conversation->type == PURPLE_CONVERSATION_TYPE_CHANNEL;
}

gboolean
purple_conversation_is_thread(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);

	return conversation->type == PURPLE_CONVERSATION_TYPE_THREAD;
}

void
purple_conversation_present(PurpleConversation *conversation) {
	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	g_signal_emit(conversation, signals[SIG_PRESENT], 0);
}

const char *
purple_conversation_get_id(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	return conversation->id;
}

char *
purple_conversation_get_global_id(PurpleConversation *conversation) {
	const char *account_id = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	account_id = purple_account_get_id(conversation->account);

	return g_strdup_printf("%s-%s", account_id, conversation->id);
}

PurpleConversationType
purple_conversation_get_conversation_type(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation),
	                     PURPLE_CONVERSATION_TYPE_UNSET);

	return conversation->type;
}

PurpleAccount *
purple_conversation_get_account(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	return conversation->account;
}

PurpleConnection *
purple_conversation_get_connection(PurpleConversation *conversation) {
	PurpleAccount *account;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	account = purple_conversation_get_account(conversation);

	if(account == NULL) {
		return NULL;
	}

	return purple_account_get_connection(account);
}

void
purple_conversation_set_title(PurpleConversation *conversation,
                              const char *title)
{
	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	if(g_set_str(&conversation->title, title)) {
		GObject *obj = G_OBJECT(conversation);

		/* We have to g_object_freeze_notify here because we're modifying more
		 * than one property. However, purple_conversation_generate_title will
		 * also have called g_object_freeze_notify before calling us because it
		 * needs to set the title-generated property to TRUE even though we set
		 * it to FALSE here. We do this, because we didn't want to write
		 * additional API that skips that part.
		 */
		g_object_freeze_notify(obj);
		g_object_notify_by_pspec(obj, properties[PROP_TITLE]);
		g_object_notify_by_pspec(obj, properties[PROP_TITLE_FOR_DISPLAY]);
		purple_conversation_set_title_generated(conversation, FALSE);
		g_object_thaw_notify(obj);
	}
}

const char *
purple_conversation_get_title(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	return conversation->title;
}

void
purple_conversation_generate_title(PurpleConversation *conversation) {
	PurpleAccount *account = NULL;
	PurpleContactInfo *account_info = NULL;
	GString *str = NULL;
	guint n_members = 0;
	gboolean first = TRUE;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	if(conversation->type != PURPLE_CONVERSATION_TYPE_DM &&
	   conversation->type != PURPLE_CONVERSATION_TYPE_GROUP_DM)
	{
		g_warning("purple_conversation_generate_title called for non DM/Group "
		          "DM conversation");

		return;
	}

	account = purple_conversation_get_account(conversation);
	account_info = purple_account_get_contact_info(account);

	str = g_string_new("");

	n_members = g_list_model_get_n_items(G_LIST_MODEL(conversation->members));
	for(guint i = 0; i < n_members; i++) {
		PurpleContactInfo *info = NULL;
		PurpleConversationMember *member = NULL;
		const char *name = NULL;

		member = g_list_model_get_item(G_LIST_MODEL(conversation->members), i);
		info = purple_conversation_member_get_contact_info(member);
		if(purple_contact_info_compare(info, account_info) == 0) {
			g_clear_object(&member);

			continue;
		}

		name = purple_contact_info_get_name_for_display(info);
		if(purple_strempty(name)) {
			g_warning("contact %p has no displayable name", info);

			g_clear_object(&member);

			continue;
		}

		if(!first) {
			g_string_append_printf(str, ", %s", name);
		} else {
			g_string_append(str, name);
			first = FALSE;
		}

		g_clear_object(&member);
	}

	/* If we found at least 1 user to add, then we set the title. */
	if(!first) {
		GObject *obj = G_OBJECT(conversation);

		g_object_freeze_notify(obj);
		purple_conversation_set_title(conversation, str->str);
		purple_conversation_set_title_generated(conversation, TRUE);
		g_object_thaw_notify(obj);
	}

	g_string_free(str, TRUE);
}

gboolean
purple_conversation_get_title_generated(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);

	return conversation->title_generated;
}

void
purple_conversation_set_name(PurpleConversation *conversation,
                             const char *name)
{
	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	if(g_set_str(&conversation->name, name)) {
		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_NAME]);
	}
}

const char *
purple_conversation_get_name(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	return conversation->name;
}

void
purple_conversation_write_message(PurpleConversation *conversation,
                                  PurpleMessage *message)
{
	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));
	g_return_if_fail(message != NULL);

	if(conversation->logging) {
		GError *error = NULL;
		PurpleHistoryManager *manager = NULL;
		gboolean success = FALSE;

		manager = purple_history_manager_get_default();
		/* We should probably handle this error somehow, but I don't think that
		 * spamming purple_debug_warning is necessarily the right call.
		 */
		success = purple_history_manager_write(manager, conversation, message,
		                                     &error);
		if(!success){
			purple_debug_info("conversation",
			                  "history manager write returned error: %s",
			                  error->message);

			g_clear_error(&error);
		}
	}

	g_list_store_append(conversation->messages, message);
}

void
purple_conversation_send_message_async(PurpleConversation *conversation,
                                       PurpleMessage *message,
                                       GCancellable *cancellable,
                                       GAsyncReadyCallback callback,
                                       gpointer data)
{
	PurpleAccount *account = NULL;
	PurpleProtocol *protocol = NULL;
	GTask *task = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));
	g_return_if_fail(PURPLE_IS_MESSAGE(message));

	purple_conversation_set_typing_state(conversation,
	                                     PURPLE_TYPING_STATE_NONE);

	task = g_task_new(conversation, cancellable, callback, data);
	g_task_set_source_tag(task, purple_conversation_send_message_async);
	g_task_set_task_data(task, g_object_ref(message), g_object_unref);

	account = purple_conversation_get_account(conversation);
	protocol = purple_account_get_protocol(account);

	if(!PURPLE_IS_PROTOCOL_CONVERSATION(protocol)) {
		g_task_return_new_error(task, PURPLE_CONVERSATION_DOMAIN, 0,
		                        "protocol does not implement "
		                        "PurpleProtocolConversation");

		g_clear_object(&task);

		return;
	}

	purple_protocol_conversation_send_message_async(PURPLE_PROTOCOL_CONVERSATION(protocol),
	                                                conversation,
	                                                message,
	                                                cancellable,
	                                                purple_conversation_send_message_async_cb,
	                                                task);
}

gboolean
purple_conversation_send_message_finish(PurpleConversation *conversation,
                                        GAsyncResult *result,
                                        GError **error)
{
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);
	g_return_val_if_fail(G_IS_ASYNC_RESULT(result), FALSE);

	g_return_val_if_fail(g_task_get_source_tag(G_TASK(result)) !=
	                     purple_conversation_send_message_async, FALSE);

	return g_task_propagate_boolean(G_TASK(result), error);
}

gboolean
purple_conversation_has_focus(PurpleConversation *conversation) {
	gboolean ret = FALSE;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);

	return ret;
}

gboolean
purple_conversation_get_age_restricted(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);

	return conversation->age_restricted;
}

void
purple_conversation_set_age_restricted(PurpleConversation *conversation,
                                       gboolean age_restricted)
{
	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	if(conversation->age_restricted != age_restricted) {
		conversation->age_restricted = age_restricted;

		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_AGE_RESTRICTED]);
	}
}

const char *
purple_conversation_get_description(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	return conversation->description;
}

void
purple_conversation_set_description(PurpleConversation *conversation,
                                    const char *description)
{
	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	if(g_set_str(&conversation->description, description)) {
		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_DESCRIPTION]);
	}
}

const char *
purple_conversation_get_topic(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	return conversation->topic;
}

void
purple_conversation_set_topic(PurpleConversation *conversation,
                              const char *topic)
{
	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	if(g_set_str(&conversation->topic, topic)) {
		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_TOPIC]);
	}
}

void
purple_conversation_set_topic_full(PurpleConversation *conversation,
                                   const char *topic,
                                   PurpleContactInfo *author,
                                   GDateTime *updated)
{
	GObject *obj = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	obj = G_OBJECT(conversation);
	g_object_freeze_notify(obj);

	purple_conversation_set_topic(conversation, topic);
	purple_conversation_set_topic_author(conversation, author);
	purple_conversation_set_topic_updated(conversation, updated);

	g_object_thaw_notify(obj);
}


PurpleContactInfo *
purple_conversation_get_topic_author(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	return conversation->topic_author;
}

void
purple_conversation_set_topic_author(PurpleConversation *conversation,
                                     PurpleContactInfo *author)
{
	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	if(g_set_object(&conversation->topic_author, author)) {
		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_TOPIC_AUTHOR]);
	}
}

GDateTime *
purple_conversation_get_topic_updated(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	return conversation->topic_updated;
}

void
purple_conversation_set_topic_updated(PurpleConversation *conversation,
                                      GDateTime *updated)
{
	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	if(birb_date_time_set(&conversation->topic_updated, updated)) {
		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_TOPIC_UPDATED]);
	}
}

const char *
purple_conversation_get_user_nickname(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	return conversation->user_nickname;
}

void
purple_conversation_set_user_nickname(PurpleConversation *conversation,
                                      const char *nickname)
{
	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	if(g_set_str(&conversation->user_nickname, nickname)) {
		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_USER_NICKNAME]);
	}
}

gboolean
purple_conversation_get_favorite(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);

	return conversation->favorite;
}

void
purple_conversation_set_favorite(PurpleConversation *conversation,
                                 gboolean favorite)
{
	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	if(conversation->favorite != favorite) {
		conversation->favorite = favorite;

		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_FAVORITE]);
	}
}

GDateTime *
purple_conversation_get_created_on(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	return conversation->created_on;
}

void
purple_conversation_set_created_on(PurpleConversation *conversation,
                                   GDateTime *created_on)
{
	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	if(birb_date_time_set(&conversation->created_on, created_on)) {
		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_CREATED_ON]);
	}
}

PurpleContactInfo *
purple_conversation_get_creator(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	return conversation->creator;
}

void
purple_conversation_set_creator(PurpleConversation *conversation,
                                PurpleContactInfo *creator)
{
	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	if(g_set_object(&conversation->creator, creator)) {
		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_CREATOR]);
	}
}

gboolean
purple_conversation_get_online(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);

	return conversation->online;
}

void
purple_conversation_set_online(PurpleConversation *conversation,
                               gboolean online)
{
	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	if(conversation->online != online) {
		conversation->online = online;

		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_ONLINE]);
	}
}

gboolean
purple_conversation_get_federated(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);

	return conversation->federated;
}

PurpleTags *
purple_conversation_get_tags(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	return conversation->tags;
}

PurpleConversationMembers *
purple_conversation_get_members(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	return conversation->members;
}

GListModel *
purple_conversation_get_messages(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	if(G_IS_LIST_MODEL(conversation->messages)) {
		return G_LIST_MODEL(conversation->messages);
	}

	return NULL;
}

PurpleAvatar *
purple_conversation_get_avatar(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	return conversation->avatar;
}

void
purple_conversation_set_avatar(PurpleConversation *conversation,
                               PurpleAvatar *avatar)
{
	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	if(g_set_object(&conversation->avatar, avatar)) {
		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_AVATAR]);
	}
}

const char *
purple_conversation_get_title_for_display(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	if(!purple_strempty(conversation->alias)) {
		return conversation->alias;
	}

	if(!purple_strempty(conversation->title)) {
		return conversation->title;
	}

	return conversation->id;
}

const char *
purple_conversation_get_alias(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	return conversation->alias;
}

void
purple_conversation_set_alias(PurpleConversation *conversation,
                              const char *alias)
{
	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	if(g_set_str(&conversation->alias, alias)) {
		GObject *obj = G_OBJECT(conversation);

		g_object_freeze_notify(obj);
		g_object_notify_by_pspec(obj, properties[PROP_ALIAS]);
		g_object_notify_by_pspec(obj, properties[PROP_TITLE_FOR_DISPLAY]);
		g_object_thaw_notify(obj);
	}
}

gboolean
purple_conversation_get_needs_attention(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);

	return conversation->needs_attention;
}

void
purple_conversation_set_needs_attention(PurpleConversation *conversation,
                                        gboolean needs_attention)
{
	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	if(conversation->needs_attention != needs_attention) {
		conversation->needs_attention = needs_attention;

		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_NEEDS_ATTENTION]);
	}
}

PurpleTypingState
purple_conversation_get_typing_state(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation),
	                     PURPLE_TYPING_STATE_NONE);

	return conversation->typing_state;
}

void
purple_conversation_set_typing_state(PurpleConversation *conversation,
                                     PurpleTypingState typing_state)
{
	gboolean send = FALSE;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	/* Remove the old timeout because we have new activity. */
	g_clear_handle_id(&conversation->typing_state_source, g_source_remove);

	/* We set some default timeouts based on the state. If the new state is
	 * TYPING, we use a 6 second timeout that will change the state to PAUSED.
	 * When the state changes to PAUSED we will set a 30 second timeout that
	 * will change the state to NONE.
	 *
	 * This allows the user interface to just tell libpurple when the user is
	 * typing, and the rest happens automatically.
	 */
	if(typing_state == PURPLE_TYPING_STATE_TYPING) {
		GDateTime *now = NULL;

		conversation->typing_state_source =
			g_timeout_add_seconds_once(6,
			                           purple_conversation_typing_state_typing_cb,
			                           conversation);

		/* We don't want to spam services with typing notifications, so we only
		 * send them if it's been at least 3 seconds since the last one was
		 * sent.
		 *
		 * Use local time because this is local to the user and we might want
		 * to output this during debug or something, and a local time stamp
		 * will make a lot more sense then.
		 */
		now = g_date_time_new_now_local();
		if(conversation->last_typing != NULL) {
			GTimeSpan difference = 0;

			difference = g_date_time_difference(now, conversation->last_typing);
			birb_date_time_clear(&conversation->last_typing);

			if(difference >= 3 * G_TIME_SPAN_SECOND) {
				send = TRUE;
			}
		}

		conversation->last_typing = now;
	} else if(typing_state == PURPLE_TYPING_STATE_PAUSED) {
		conversation->typing_state_source =
			g_timeout_add_seconds_once(30,
			                           purple_conversation_typing_state_paused_cb,
			                           conversation);
	} else if(typing_state == PURPLE_TYPING_STATE_NONE) {
		birb_date_time_clear(&conversation->last_typing);
	}

	if(conversation->typing_state != typing_state) {
		conversation->typing_state = typing_state;

		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_TYPING_STATE]);

		/* The state changed so we need to send it. */
		send = TRUE;
	}

	/* Check if we have a protocol that implements
	 * ProtocolConversation.send_typing and call it if it does.
	 *
	 * We do this after the notify above to make sure the user interface will
	 * not be possibly blocked by the protocol.
	 */
	if(send && PURPLE_IS_ACCOUNT(conversation->account)) {
		PurpleProtocol *protocol = NULL;

		protocol = purple_account_get_protocol(conversation->account);
		if(PURPLE_IS_PROTOCOL_CONVERSATION(protocol)) {
			PurpleProtocolConversation *protocol_conversation = NULL;

			protocol_conversation = PURPLE_PROTOCOL_CONVERSATION(protocol);

			if(purple_protocol_conversation_implements_send_typing(protocol_conversation))
			{
				purple_protocol_conversation_send_typing(protocol_conversation,
				                                         conversation,
				                                         conversation->typing_state);
			}
		}
	}
}

gboolean
purple_conversation_get_logging(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);

	return conversation->logging;
}

void
purple_conversation_set_logging(PurpleConversation *conversation,
                                gboolean logging)
{
	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	if(conversation->logging != logging) {
		conversation->logging = logging;

		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_LOGGING]);
	}
}

gboolean
purple_conversation_get_drafting(PurpleConversation *conversation) {
	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);

	return conversation->drafting;
}

void
purple_conversation_set_drafting(PurpleConversation *conversation,
                                 gboolean drafting)
{
	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	if(conversation->drafting != drafting) {
		conversation->drafting = drafting;

		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_DRAFTING]);
	}
}

mercurial