# HG changeset patch
# Node ID 7517eb21f027
# Parent  d6ed63c2508e
diff --git a/libpurple/meson.build b/libpurple/meson.build
--- a/libpurple/meson.build
+++ b/libpurple/meson.build
@@ -68,6 +68,7 @@
 	'purplemarkup.c',
 	'purplemenu.c',
 	'purplemessage.c',
+	'purplemessagemodel.c',
 	'purplenoopcredentialprovider.c',
 	'purplenotification.c',
 	'purplenotificationmanager.c',
@@ -193,6 +194,7 @@
 	'purplemarkup.h',
 	'purplemenu.h',
 	'purplemessage.h',
+	'purplemessagemodel.h',
 	'purplenoopcredentialprovider.h',
 	'purplenotification.h',
 	'purplenotificationmanager.h',
diff --git a/libpurple/purplemessagemodel.h b/libpurple/purplemessagemodel.h
new file mode 100644
--- /dev/null
+++ b/libpurple/purplemessagemodel.h
@@ -0,0 +1,318 @@
+/*
+ * 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/>.
+ */
+
+#if !defined(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION)
+# error "only <purple.h> may be included directly"
+#endif
+
+#ifndef PURPLE_MESSAGE_MODEL_H
+#define PURPLE_MESSAGE_MODEL_H
+
+#include <glib-object.h>
+
+#include "purpleattachment.h"
+#include "purplecontact.h"
+#include "purpleversion.h"
+
+G_BEGIN_DECLS
+
+/**
+ * PurpleMessageModel:
+ *
+ * An interface describing a message.
+ *
+ * Since: 3.0
+ */
+
+#define PURPLE_TYPE_MESSAGE_MODEL (purple_message_model_get_type())
+
+PURPLE_AVAILABLE_IN_3_0
+G_DECLARE_INTERFACE(PurpleMessageModel, purple_message_model, PURPLE,
+                    MESSAGE_MODEL, GObject)
+
+struct _PurpleMessageModelInterface {
+	/*< private >*/
+	GTypeInterface parent;
+
+	/*< private >*/
+	gpointer reserved[4];
+};
+
+/**
+ * purple_message_model_get_action:
+ * @model: The instance.
+ *
+ * Gets whether or not the message is considered an action.
+ *
+ * Returns: %TRUE if the message is an action, otherwise %FALSE.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+gboolean purple_message_model_get_action(PurpleMessageModel *model);
+
+/**
+ * purple_message_model_set_action:
+ * @model: The instance.
+ * @action: Whether or not this message is an action.
+ *
+ * Sets whether or not the message is considered an action.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+void purple_message_model_set_action(PurpleMessageModel *model, gboolean action);
+
+/**
+ * purple_message_model_get_author:
+ * @model: The instance.
+ *
+ * Gets the author of the message.
+ *
+ * Returns: (transfer full) (nullable): The author of the message if set,
+ *          otherwise %NULL.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+PurpleContact *purple_message_model_get_author(PurpleMessageModel *model);
+
+/**
+ * purple_message_model_set_author:
+ * @model: The instance.
+ * @author: (transfer none) (nullable): The new author.
+ *
+ * Sets the author of the message to @author.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+void purple_message_model_set_author(PurpleMessageModel *model, PurpleContact *author);
+
+/**
+ * purple_message_model_get_contents:
+ * @model: The instance.
+ *
+ * Gets the text contents of the message.
+ *
+ * Returns: (transfer full) (nullable): The text contents of the message.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+char *purple_message_model_get_contents(PurpleMessageModel *model);
+
+/**
+ * purple_message_model_set_contents:
+ * @model: The instance.
+ * @contents: (transfer none) (nullable): The new contents.
+ *
+ * Sets the contents of message to @contents.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+void purple_message_model_set_contents(PurpleMessageModel *model, const char *contents);
+
+/**
+ * purple_message_model_get_delivered:
+ * @model: The instance.
+ *
+ * Gets whether or not the message has been delivered.
+ *
+ * Returns: %TRUE if the message has been delivered, otherwise %FALSE.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+gboolean purple_message_model_get_delivered(PurpleMessageModel *model);
+
+/**
+ * purple_message_model_set_delivered:
+ * @model: The instance.
+ * @delivered: %TRUE to mark the message as delivered, otherwise %FALSE.
+ *
+ * Sets the delivery status of the message.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+void purple_message_model_set_delivered(PurpleMessageModel *model, gboolean delivered);
+
+/**
+ * purple_message_model_get_delivered_at:
+ * @model: The instance.
+ *
+ * Gets the timestamp of when the message was delivered.
+ *
+ * Returns: (transfer full) (nullable): The timestamp when the message was
+ *          delivered.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+GDateTime *purple_message_model_get_delivered_at(PurpleMessageModel *model);
+
+/**
+ * purple_message_model_set_delivered_at:
+ * @model: The instance.
+ * @delivered_at: (transfer none) (nullable): The new timestamp of when the
+ *             message was delivered.
+ *
+ * Sets the delivered at timestamp of the message to @delivered_at.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+void purple_message_model_set_delivered_at(PurpleMessageModel *model, GDateTime *delivered_at);
+
+/**
+ * purple_message_model_get_edited:
+ * @model: The instance.
+ *
+ * Gets whether or not the message has been edited.
+ *
+ * Returns: %TRUE if the message has been edited, otherwise %FALSE.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+gboolean purple_message_model_get_edited(PurpleMessageModel *model);
+
+/**
+ * purple_message_model_set_edited:
+ * @model: The instance.
+ * @edited: %TRUE to mark the message as edited, otherwise %FALSE.
+ *
+ * Sets whether or not the message has been edited.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+void purple_message_model_set_edited(PurpleMessageModel *model, gboolean edited);
+
+/**
+ * purple_message_model_get_edited_at:
+ * @model: The instance.
+ *
+ * Gets the timestamp of when the message was last edited.
+ *
+ * Returns: (transfer full) (nullable): The timestamp when the message was
+ *          edited.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+GDateTime *purple_message_model_get_edited_at(PurpleMessageModel *model);
+
+/**
+ * purple_message_model_set_edited_at:
+ * @model: The instance.
+ * @edited_at: (transfer none) (nullable): The new timestamp of when the
+ *             message was edited.
+ *
+ * Sets the edited at timestamp of the message to @edited_at.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+void purple_message_model_set_edited_at(PurpleMessageModel *model, GDateTime *edited_at);
+
+/**
+ * purple_message_model_get_error:
+ * @model: The instance.
+ *
+ * Gets the error from the message.
+ *
+ * Returns: (transfer full) (nullable): The error from the message if any.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+GError *purple_message_model_get_error(PurpleMessageModel *model);
+
+/**
+ * purple_message_model_set_error:
+ * @model: The instance.
+ * @error: (transfer none) (nullable): The new error.
+ *
+ * Sets the error for the message to @error.
+ *
+ * Since 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+void purple_message_model_set_error(PurpleMessageModel *model, GError *error);
+
+/**
+ * purple_message_model_get_id:
+ * @model: The instance.
+ *
+ * Gets the id of the message.
+ *
+ * Returns: (transfer full) (nullable): The id of the message if set, otherwise
+ *          %NULL.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+char *purple_message_model_get_id(PurpleMessageModel *model);
+
+/**
+ * purple_message_model_set_id:
+ * @model: The instance.
+ * @id: (nullable): The new identifier to set.
+ *
+ * Sets the identifier of the message to @id.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+void purple_message_model_set_id(PurpleMessageModel *model, const char *id);
+
+/**
+ * purple_message_model_get_timestamp:
+ * @model: The instance.
+ *
+ * Gets the timestamp of the message.
+ *
+ * Returns: (transfer full) (nullable): The timestamp.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+GDateTime *purple_message_model_get_timestamp(PurpleMessageModel *model);
+
+/**
+ * purple_message_model_set_timestamp:
+ * @model: The instance.
+ * @timestamp: (nullable) (transfer none): The new timestamp.
+ *
+ * Sets the timestamp of the message to @timestamp.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+void purple_message_model_set_timestamp(PurpleMessageModel *model, GDateTime *timestamp);
+
+G_END_DECLS
+
+#endif /* PURPLE_MESSAGE_MODEL_H */
diff --git a/libpurple/purplemessagemodel.c b/libpurple/purplemessagemodel.c
new file mode 100644
--- /dev/null
+++ b/libpurple/purplemessagemodel.c
@@ -0,0 +1,419 @@
+/*
+ * 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 <birb.h>
+
+#include "purplemessagemodel.h"
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+G_DEFINE_INTERFACE(PurpleMessageModel, purple_message_model, G_TYPE_OBJECT)
+
+static void
+purple_message_model_default_init(PurpleMessageModelInterface *iface) {
+	GParamSpec *pspec = NULL;
+
+	/**
+	 * PurpleMessageModel:action:
+	 *
+	 * Whether or not the message is an action.
+	 *
+	 * Typically a message is considered an action when the body starts with
+	 * `/me`. Some protocols handle this differently but this is mostly a user
+	 * interface hint that this message is different than a normal text
+	 * message.
+	 *
+	 * Since: 3.0
+	 */
+	pspec = g_param_spec_boolean(
+		"action", "action",
+		"Whether or not the message is an action.",
+		FALSE,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+	g_object_interface_install_property(iface, pspec);
+
+	/**
+	 * PurpleMessageModel:author:
+	 *
+	 * The author of the message.
+	 *
+	 * Since: 3.0
+	 */
+	pspec = g_param_spec_object(
+		"author", "author",
+		"The author of the message.",
+		PURPLE_TYPE_CONTACT,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+	g_object_interface_install_property(iface, pspec);
+
+	/**
+	 * PurpleMessageModel:contents:
+	 *
+	 * The text contents of the message.
+	 *
+	 * Since: 3.0
+	 */
+	pspec = g_param_spec_string(
+		"contents", "contents",
+		"The text contents of the message.",
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+	g_object_interface_install_property(iface, pspec);
+
+	/**
+	 * PurpleMessageModel:delivered:
+	 *
+	 * Whether or not this message has been delivered.
+	 *
+	 * This is a computed property from [property@MessageModel:delivered-at].
+	 * The getter checks if delivered-at is set, and the setter will use
+	 * [ctor@GLib.DateTime.new_now_utc] or %NULL.
+	 *
+	 * Since: 3.0
+	 */
+	pspec = g_param_spec_boolean(
+		"delivered", "delivered",
+		"Whether or not the message has been delivered.",
+		FALSE,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+	g_object_interface_install_property(iface, pspec);
+
+	/**
+	 * PurpleMessageModel:delivered-at:
+	 *
+	 * The time that the message was delivered. This is protocol dependent and
+	 * possibly client dependent as well. So if this is %NULL that doesn't
+	 * necessarily mean the message was not delivered.
+	 *
+	 * Since: 3.0
+	 */
+	pspec = g_param_spec_boxed(
+		"delivered-at", "delivered-at",
+		"The time that the message was delivered at.",
+		G_TYPE_DATE_TIME,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+	g_object_interface_install_property(iface, pspec);
+
+	/**
+	 * PurpleMessageModel:edited:
+	 *
+	 * Whether or not this message has been edited.
+	 *
+	 * This is a computed property from [property@MessageModel:edited-at]. The
+	 * getter checks if edited-at is set, and the setter will use
+	 * [ctor@GLib.DateTime.new_now_utc] or %NULL.
+	 *
+	 * Since: 3.0
+	 */
+	pspec = g_param_spec_boolean(
+		"edited", "edited",
+		"Whether or not the message has been edited.",
+		FALSE,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+	g_object_interface_install_property(iface, pspec);
+
+	/**
+	 * PurpleMessageModel:edited-at:
+	 *
+	 * The time that the message was last edited at. This is protocol dependent
+	 * and possibly client dependent as well. So if this is %NULL that doesn't
+	 * necessarily mean the message was not edited.
+	 *
+	 * Since: 3.0
+	 */
+	pspec = g_param_spec_boxed(
+		"edited-at", "edited-at",
+		"The time that the message was last edited.",
+		G_TYPE_DATE_TIME,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+	g_object_interface_install_property(iface, pspec);
+
+	/**
+	 * PurpleMessageModel:error:
+	 *
+	 * An error for the message.
+	 *
+	 * Typically this is just for delivery failures, but this could be any kind
+	 * of error.
+	 *
+	 * Since: 3.0
+	 */
+	pspec = g_param_spec_boxed(
+		"error", "error",
+		"An error that the message encountered.",
+		G_TYPE_ERROR,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+	g_object_interface_install_property(iface, pspec);
+
+	/**
+	 * PurpleMessageModel:id:
+	 *
+	 * The identifier of the message.
+	 *
+	 * Since: 3.0
+	 */
+	pspec = g_param_spec_string(
+		"id", "id",
+		"The identifier of the message.",
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+	g_object_interface_install_property(iface, pspec);
+
+	/**
+	 * PurpleMessageModel:timestamp:
+	 *
+	 * The timestamp of the message.
+	 *
+	 * Since: 3.0
+	 */
+	pspec = g_param_spec_boxed(
+		"timestamp", "timestamp",
+		"The timestamp of the message",
+		G_TYPE_DATE_TIME,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+	g_object_interface_install_property(iface, pspec);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+gboolean
+purple_message_model_get_action(PurpleMessageModel *model) {
+	gboolean action = FALSE;
+
+	g_return_val_if_fail(PURPLE_IS_MESSAGE_MODEL(model), FALSE);
+
+	g_object_get(model, "action", &action, NULL);
+
+	return action;
+}
+
+void
+purple_message_model_set_action(PurpleMessageModel *model, gboolean action) {
+	g_return_if_fail(PURPLE_IS_MESSAGE_MODEL(model));
+
+	g_object_set(model, "action", action, NULL);
+}
+
+PurpleContact *
+purple_message_model_get_author(PurpleMessageModel *model) {
+	PurpleContact *author = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_MESSAGE_MODEL(model), NULL);
+
+	g_object_get(model, "author", &author, NULL);
+
+	return author;
+}
+
+void
+purple_message_model_set_author(PurpleMessageModel *model,
+                                PurpleContact *author)
+{
+	g_return_if_fail(PURPLE_IS_MESSAGE_MODEL(model));
+
+	g_object_set(model, "author", author, NULL);
+}
+
+char *
+purple_message_model_get_contents(PurpleMessageModel *model) {
+	char *contents = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_MESSAGE_MODEL(model), NULL);
+
+	g_object_get(model, "contents", &contents, NULL);
+
+	return contents;
+}
+
+void
+purple_message_model_set_contents(PurpleMessageModel *model,
+                                  const char *contents)
+{
+	g_return_if_fail(PURPLE_IS_MESSAGE_MODEL(model));
+
+	g_object_set(model, "contents", contents, NULL);
+}
+
+gboolean
+purple_message_model_get_delivered(PurpleMessageModel *model) {
+	GDateTime *dt = NULL;
+	gboolean ret = FALSE;
+
+	g_return_val_if_fail(PURPLE_IS_MESSAGE_MODEL(model), FALSE);
+
+	dt = purple_message_model_get_delivered_at(model);
+
+	if(dt != NULL) {
+		ret = TRUE;
+	}
+
+	birb_date_time_clear(&dt);
+
+	return ret;
+}
+
+void
+purple_message_model_set_delivered(PurpleMessageModel *model,
+                                   gboolean delivered)
+{
+	GDateTime *dt = NULL;
+
+	g_return_if_fail(PURPLE_IS_MESSAGE_MODEL(model));
+
+	if(delivered) {
+		dt = g_date_time_new_now_utc();
+	}
+
+	purple_message_model_set_delivered_at(model, dt);
+
+	birb_date_time_clear(&dt);
+}
+
+GDateTime *
+purple_message_model_get_delivered_at(PurpleMessageModel *model) {
+	GDateTime *delivered_at = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_MESSAGE_MODEL(model), NULL);
+
+	g_object_get(model, "delivered-at", &delivered_at, NULL);
+
+	return delivered_at;
+}
+
+void
+purple_message_model_set_delivered_at(PurpleMessageModel *model,
+                                      GDateTime *delivered_at)
+{
+	g_return_if_fail(PURPLE_IS_MESSAGE_MODEL(model));
+
+	g_object_set(model, "delivered-at", delivered_at, NULL);
+}
+
+gboolean
+purple_message_model_get_edited(PurpleMessageModel *model) {
+	GDateTime *edited_at = NULL;
+	gboolean ret = FALSE;
+
+	g_return_val_if_fail(PURPLE_IS_MESSAGE_MODEL(model), FALSE);
+
+	edited_at = purple_message_model_get_edited_at(model);
+	if(edited_at != NULL) {
+		ret = TRUE;
+	}
+
+	birb_date_time_clear(&edited_at);
+
+	return ret;
+}
+
+void
+purple_message_model_set_edited(PurpleMessageModel *model, gboolean edited) {
+	GDateTime *dt = NULL;
+
+	g_return_if_fail(PURPLE_IS_MESSAGE_MODEL(model));
+
+	if(edited) {
+		dt = g_date_time_new_now_utc();
+	}
+
+	purple_message_model_set_edited_at(model, dt);
+
+	birb_date_time_clear(&dt);
+}
+
+GDateTime *
+purple_message_model_get_edited_at(PurpleMessageModel *model) {
+	GDateTime *edited_at = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_MESSAGE_MODEL(model), NULL);
+
+	g_object_get(model, "edited-at", &edited_at, NULL);
+
+	return edited_at;
+}
+
+void
+purple_message_model_set_edited_at(PurpleMessageModel *model,
+                                   GDateTime *edited_at)
+{
+	g_return_if_fail(PURPLE_IS_MESSAGE_MODEL(model));
+
+	g_object_set(model, "edited-at", edited_at, NULL);
+}
+
+GError *
+purple_message_model_get_error(PurpleMessageModel *model) {
+	GError *error = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_MESSAGE_MODEL(model), NULL);
+
+	g_object_get(model, "error", &error, NULL);
+
+	return error;
+}
+
+void
+purple_message_model_set_error(PurpleMessageModel *model, GError *error) {
+	g_return_if_fail(PURPLE_IS_MESSAGE_MODEL(model));
+
+	g_object_set(model, "error", error, NULL);
+}
+
+char *
+purple_message_model_get_id(PurpleMessageModel *model) {
+	char *id = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_MESSAGE_MODEL(model), NULL);
+
+	g_object_get(model, "id", &id, NULL);
+
+	return id;
+}
+
+void
+purple_message_model_set_id(PurpleMessageModel *model, const char *id) {
+	g_return_if_fail(PURPLE_IS_MESSAGE_MODEL(model));
+
+	g_object_set(model, "id", id, NULL);
+}
+
+GDateTime *
+purple_message_model_get_timestamp(PurpleMessageModel *model) {
+	GDateTime *timestamp = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_MESSAGE_MODEL(model), NULL);
+
+	g_object_get(model, "timestamp", &timestamp, NULL);
+
+	return timestamp;
+}
+
+void
+purple_message_model_set_timestamp(PurpleMessageModel *model,
+                                   GDateTime *timestamp)
+{
+	g_return_if_fail(PURPLE_IS_MESSAGE_MODEL(model));
+
+	g_object_set(model, "timestamp", timestamp, NULL);
+}
diff --git a/libpurple/tests/meson.build b/libpurple/tests/meson.build
--- a/libpurple/tests/meson.build
+++ b/libpurple/tests/meson.build
@@ -22,6 +22,7 @@
     'markup',
     'menu',
     'message',
+    'message_model',
     'notification',
     'notification_manager',
     'person',
diff --git a/libpurple/tests/test_message_model.c b/libpurple/tests/test_message_model.c
new file mode 100644
--- /dev/null
+++ b/libpurple/tests/test_message_model.c
@@ -0,0 +1,570 @@
+/*
+ * 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.h>
+
+#include <birb.h>
+
+#include <purple.h>
+
+#include "test_ui.h"
+
+#define TEST_PURPLE_MESSAGE_MODEL_DOMAIN (g_quark_from_static_string("test-message-model"))
+
+/******************************************************************************
+ * Implementation
+ *****************************************************************************/
+G_DECLARE_FINAL_TYPE(TestPurpleMessageModel, test_purple_message_model,
+                     TEST_PURPLE, MESSAGE_MODEL, GObject)
+
+enum {
+	N_PROPERTIES,
+	PROP_ACTION,
+	PROP_AUTHOR,
+	PROP_CONTENTS,
+	PROP_DELIVERED,
+	PROP_DELIVERED_AT,
+	PROP_EDITED,
+	PROP_EDITED_AT,
+	PROP_ERROR,
+	PROP_ID,
+	PROP_TIMESTAMP,
+};
+
+struct _TestPurpleMessageModel {
+	GObject parent;
+
+	gboolean action;
+	PurpleContact *author;
+	char *contents;
+	GDateTime *delivered_at;
+	GDateTime *edited_at;
+	GError *error;
+	char *id;
+	GDateTime *timestamp;
+};
+
+static gboolean
+test_purple_message_model_get_action(TestPurpleMessageModel *model) {
+	return model->action;
+}
+
+static void
+test_purple_message_model_set_action(TestPurpleMessageModel *model,
+                                     gboolean action)
+{
+	if(action != model->action) {
+		model->action = action;
+
+		g_object_notify(G_OBJECT(model), "action");
+	}
+}
+
+static PurpleContact *
+test_purple_message_model_get_author(TestPurpleMessageModel *model) {
+	if(PURPLE_IS_CONTACT(model->author)) {
+		return g_object_ref(model->author);
+	}
+
+	return NULL;
+}
+
+static void
+test_purple_message_model_set_author(TestPurpleMessageModel *model,
+                                     PurpleContact *author)
+{
+	if(g_set_object(&model->author, author)) {
+		g_object_notify(G_OBJECT(model), "author");
+	}
+}
+
+static char *
+test_purple_message_model_get_contents(TestPurpleMessageModel *model) {
+	return g_strdup(model->contents);
+}
+
+static void
+test_purple_message_model_set_contents(TestPurpleMessageModel *model,
+                                       const char *contents)
+{
+	if(!purple_strequal(contents, model->contents)) {
+		g_free(model->contents);
+		model->contents = g_strdup(contents);
+
+		g_object_notify(G_OBJECT(model), "contents");
+	}
+}
+
+static GDateTime *
+test_purple_message_model_get_delivered_at(TestPurpleMessageModel *model) {
+	if(model->delivered_at != NULL) {
+		return g_date_time_ref(model->delivered_at);
+	}
+
+	return NULL;
+}
+
+static void
+test_purple_message_model_set_delivered_at(TestPurpleMessageModel *model,
+                                           GDateTime *delivered_at)
+{
+	if(birb_date_time_set(&model->delivered_at, delivered_at)) {
+		GObject *obj = G_OBJECT(model);
+
+		g_object_freeze_notify(obj);
+		g_object_notify(obj, "delivered");
+		g_object_notify(obj, "delivered-at");
+		g_object_thaw_notify(obj);
+	}
+}
+
+static GDateTime *
+test_purple_message_model_get_edited_at(TestPurpleMessageModel *model) {
+	if(model->edited_at != NULL) {
+		return g_date_time_ref(model->edited_at);
+	}
+
+	return NULL;
+}
+
+static void
+test_purple_message_model_set_edited_at(TestPurpleMessageModel *model,
+                                        GDateTime *edited_at)
+{
+	if(birb_date_time_set(&model->edited_at, edited_at)) {
+		GObject *obj = G_OBJECT(model);
+
+		g_object_freeze_notify(obj);
+		g_object_notify(obj, "edited");
+		g_object_notify(obj, "edited-at");
+		g_object_thaw_notify(obj);
+	}
+}
+
+static GError *
+test_purple_message_model_get_error(TestPurpleMessageModel *model) {
+	if(model->error != NULL) {
+		return g_error_copy(model->error);
+	}
+
+	return NULL;
+}
+
+static void
+test_purple_message_model_set_error(TestPurpleMessageModel *model,
+                                    GError *error)
+{
+	g_clear_error(&model->error);
+	if(error != NULL) {
+		model->error = g_error_copy(error);
+	}
+
+	g_object_notify(G_OBJECT(model), "error");
+}
+
+static char *
+test_purple_message_model_get_id(TestPurpleMessageModel *model) {
+	return g_strdup(model->id);
+}
+
+static void
+test_purple_message_model_set_id(TestPurpleMessageModel *model,
+                                 const char *id)
+{
+	if(!purple_strequal(id, model->id)) {
+		g_free(model->id);
+		model->id = g_strdup(id);
+
+		g_object_notify(G_OBJECT(model), "id");
+	}
+}
+
+static GDateTime *
+test_purple_message_model_get_timestamp(TestPurpleMessageModel *model) {
+	if(model->timestamp != NULL) {
+		return g_date_time_ref(model->timestamp);
+	}
+
+	return NULL;
+}
+
+static void
+test_purple_message_model_set_timestamp(TestPurpleMessageModel *model,
+                                        GDateTime *timestamp)
+{
+	if(birb_date_time_set(&model->timestamp, timestamp)) {
+		g_object_notify(G_OBJECT(model), "timestamp");
+	}
+}
+
+static void
+test_purple_message_model_iface_init(G_GNUC_UNUSED PurpleMessageModelInterface *iface)
+{
+}
+
+G_DEFINE_FINAL_TYPE_WITH_CODE(TestPurpleMessageModel,
+                              test_purple_message_model,
+                              G_TYPE_OBJECT,
+                              G_IMPLEMENT_INTERFACE(PURPLE_TYPE_MESSAGE_MODEL,
+                                                    test_purple_message_model_iface_init))
+
+static void
+test_purple_message_model_finalize(GObject *obj) {
+	TestPurpleMessageModel *model = TEST_PURPLE_MESSAGE_MODEL(obj);
+
+	g_clear_object(&model->author);
+	g_clear_pointer(&model->contents, g_free);
+	birb_date_time_clear(&model->delivered_at);
+	birb_date_time_clear(&model->edited_at);
+	g_clear_error(&model->error);
+	g_clear_pointer(&model->id, g_free);
+	birb_date_time_clear(&model->timestamp);
+
+	G_OBJECT_CLASS(test_purple_message_model_parent_class)->finalize(obj);
+}
+
+static void
+test_purple_message_model_get_property(GObject *obj, guint param_id, GValue *value,
+                                       GParamSpec *pspec)
+{
+	TestPurpleMessageModel *model = TEST_PURPLE_MESSAGE_MODEL(obj);
+
+	switch(param_id) {
+	case PROP_ACTION:
+		g_value_set_boolean(value,
+		                    test_purple_message_model_get_action(model));
+		break;
+	case PROP_AUTHOR:
+		g_value_take_object(value,
+		                    test_purple_message_model_get_author(model));
+		break;
+	case PROP_CONTENTS:
+		g_value_take_string(value,
+		                    test_purple_message_model_get_contents(model));
+		break;
+	case PROP_DELIVERED:
+		g_value_set_boolean(value,
+		                    purple_message_model_get_delivered(PURPLE_MESSAGE_MODEL(model)));
+		break;
+	case PROP_DELIVERED_AT:
+		g_value_take_boxed(value,
+		                   test_purple_message_model_get_delivered_at(model));
+		break;
+	case PROP_EDITED:
+		g_value_set_boolean(value,
+		                    purple_message_model_get_edited(PURPLE_MESSAGE_MODEL(model)));
+		break;
+	case PROP_EDITED_AT:
+		g_value_take_boxed(value,
+		                   test_purple_message_model_get_edited_at(model));
+		break;
+	case PROP_ERROR:
+		g_value_take_boxed(value, test_purple_message_model_get_error(model));
+		break;
+	case PROP_ID:
+		g_value_take_string(value, test_purple_message_model_get_id(model));
+		break;
+	case PROP_TIMESTAMP:
+		g_value_take_boxed(value,
+		                   test_purple_message_model_get_timestamp(model));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
+	}
+}
+
+static void
+test_purple_message_model_set_property(GObject *obj, guint param_id,
+                                       const GValue *value, GParamSpec *pspec)
+{
+	TestPurpleMessageModel *model = TEST_PURPLE_MESSAGE_MODEL(obj);
+
+	switch(param_id) {
+	case PROP_ACTION:
+		test_purple_message_model_set_action(model,
+		                                     g_value_get_boolean(value));
+		break;
+	case PROP_AUTHOR:
+		test_purple_message_model_set_author(model, g_value_get_object(value));
+		break;
+	case PROP_CONTENTS:
+		test_purple_message_model_set_contents(model,
+		                                       g_value_get_string(value));
+		break;
+	case PROP_DELIVERED:
+		purple_message_model_set_delivered(PURPLE_MESSAGE_MODEL(model),
+		                                   g_value_get_boolean(value));
+		break;
+	case PROP_DELIVERED_AT:
+		test_purple_message_model_set_delivered_at(model,
+		                                           g_value_get_boxed(value));
+		break;
+	case PROP_EDITED:
+		purple_message_model_set_edited(PURPLE_MESSAGE_MODEL(model),
+		                                g_value_get_boolean(value));
+		break;
+	case PROP_EDITED_AT:
+		test_purple_message_model_set_edited_at(model,
+		                                        g_value_get_boxed(value));
+		break;
+	case PROP_ERROR:
+		test_purple_message_model_set_error(model, g_value_get_boxed(value));
+		break;
+	case PROP_ID:
+		test_purple_message_model_set_id(model, g_value_get_string(value));
+		break;
+	case PROP_TIMESTAMP:
+		test_purple_message_model_set_timestamp(model,
+		                                        g_value_get_boxed(value));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
+	}
+}
+
+static void
+test_purple_message_model_init(G_GNUC_UNUSED TestPurpleMessageModel *model) {
+}
+
+static void
+test_purple_message_model_class_init(TestPurpleMessageModelClass *klass) {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+	obj_class->finalize = test_purple_message_model_finalize;
+	obj_class->get_property = test_purple_message_model_get_property;
+	obj_class->set_property = test_purple_message_model_set_property;
+
+	g_object_class_override_property(obj_class, PROP_ACTION, "action");
+	g_object_class_override_property(obj_class, PROP_AUTHOR, "author");
+	g_object_class_override_property(obj_class, PROP_CONTENTS, "contents");
+	g_object_class_override_property(obj_class, PROP_DELIVERED, "delivered");
+	g_object_class_override_property(obj_class, PROP_DELIVERED_AT,
+	                                 "delivered-at");
+	g_object_class_override_property(obj_class, PROP_EDITED, "edited");
+	g_object_class_override_property(obj_class, PROP_EDITED_AT, "edited-at");
+	g_object_class_override_property(obj_class, PROP_ERROR, "error");
+	g_object_class_override_property(obj_class, PROP_ID, "id");
+	g_object_class_override_property(obj_class, PROP_TIMESTAMP, "timestamp");
+}
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_purple_message_model_properties(void) {
+	TestPurpleMessageModel *model = NULL;
+	PurpleAccount *account = NULL;
+	PurpleContact *author = NULL;
+	PurpleContact *author1 = NULL;
+	GDateTime *delivered_at = NULL;
+	GDateTime *delivered_at1 = NULL;
+	GDateTime *edited_at = NULL;
+	GDateTime *edited_at1 = NULL;
+	GDateTime *timestamp = NULL;
+	GDateTime *timestamp1 = NULL;
+	GError *error = NULL;
+	GError *error1 = NULL;
+	gboolean action = FALSE;
+	char *contents = NULL;
+	char *id = NULL;
+
+	account = purple_account_new("test", "test");
+	author = purple_contact_new(account, NULL);
+	error = g_error_new(TEST_PURPLE_MESSAGE_MODEL_DOMAIN, 0, "weee");
+	delivered_at = g_date_time_new_now_utc();
+	edited_at = g_date_time_new_now_utc();
+	timestamp = g_date_time_new_now_utc();
+
+	model = g_object_new(
+		test_purple_message_model_get_type(),
+		"action", TRUE,
+		"author", author,
+		"contents", "this is the message",
+		"delivered-at", delivered_at,
+		"edited-at", edited_at,
+		"error", error,
+		"id", "abc-123",
+		"timestamp", timestamp,
+		NULL);
+
+	g_clear_error(&error);
+
+	g_object_get(
+		model,
+		"action", &action,
+		"author", &author1,
+		"contents", &contents,
+		"delivered-at", &delivered_at1,
+		"edited-at", &edited_at1,
+		"error", &error1,
+		"id", &id,
+		"timestamp", &timestamp1,
+		NULL);
+
+	g_assert_true(action);
+
+	g_assert_true(author1 == author);
+	g_clear_object(&author1);
+
+	g_assert_cmpstr(contents, ==, "this is the message");
+	g_clear_pointer(&contents, g_free);
+
+	g_assert_nonnull(delivered_at1);
+	g_assert_true(g_date_time_equal(delivered_at1, delivered_at));
+	birb_date_time_clear(&delivered_at1);
+
+	g_assert_nonnull(edited_at1);
+	g_assert_true(g_date_time_equal(edited_at1, edited_at));
+	birb_date_time_clear(&edited_at1);
+
+	g_assert_error(error1, TEST_PURPLE_MESSAGE_MODEL_DOMAIN, 0);
+	g_clear_error(&error1);
+
+	g_assert_cmpstr(id, ==, "abc-123");
+	g_clear_pointer(&id, g_free);
+
+	g_assert_nonnull(timestamp1);
+	g_assert_true(g_date_time_equal(timestamp1, timestamp));
+	birb_date_time_clear(&timestamp1);
+
+	birb_date_time_clear(&delivered_at);
+	birb_date_time_clear(&edited_at);
+	birb_date_time_clear(&timestamp);
+
+	g_assert_finalize_object(model);
+	g_assert_finalize_object(author);
+	g_assert_finalize_object(account);
+}
+
+static void
+test_purple_message_model_computed_property_delivered(void) {
+	PurpleMessageModel *model = NULL;
+	GDateTime *delivered_at = NULL;
+	GDateTime *now = NULL;
+	gboolean delivered = FALSE;
+
+	model = g_object_new(test_purple_message_model_get_type(), NULL);
+	now = g_date_time_new_now_utc();
+
+	/* Make sure the defaults are good. */
+	delivered = purple_message_model_get_delivered(model);
+	g_assert_false(delivered);
+
+	delivered_at = purple_message_model_get_delivered_at(model);
+	g_assert_null(delivered_at);
+
+	/* Set the delivered property to TRUE. */
+	purple_message_model_set_delivered(model, TRUE);
+	delivered_at = purple_message_model_get_delivered_at(model);
+	g_assert_nonnull(delivered_at);
+	birb_date_time_clear(&delivered_at);
+
+	/* Set the delivered property to FALSE. */
+	purple_message_model_set_delivered(model, FALSE);
+	delivered_at = purple_message_model_get_delivered_at(model);
+	g_assert_null(delivered_at);
+
+	/* Set the delivered-at property to now. */
+	purple_message_model_set_delivered_at(model, now);
+	delivered = purple_message_model_get_delivered(model);
+	g_assert_true(delivered);
+
+	/* Set the delivered-at-property NULL. */
+	purple_message_model_set_delivered_at(model, NULL);
+	delivered = purple_message_model_get_delivered(model);
+	g_assert_false(delivered);
+
+	/* Cleanup. */
+	g_assert_finalize_object(model);
+	birb_date_time_clear(&now);
+}
+
+static void
+test_purple_message_model_computed_property_edited(void) {
+	PurpleMessageModel *model = NULL;
+	GDateTime *edited_at = NULL;
+	GDateTime *now = NULL;
+	gboolean edited = FALSE;
+
+	model = g_object_new(test_purple_message_model_get_type(), NULL);
+	now = g_date_time_new_now_utc();
+
+	/* Make sure the defaults are good. */
+	edited = purple_message_model_get_edited(model);
+	g_assert_false(edited);
+
+	edited_at = purple_message_model_get_edited_at(model);
+	g_assert_null(edited_at);
+
+	/* Set the edited property to TRUE. */
+	purple_message_model_set_edited(model, TRUE);
+	edited_at = purple_message_model_get_edited_at(model);
+	g_assert_nonnull(edited_at);
+	birb_date_time_clear(&edited_at);
+
+	/* Set the edited property to FALSE. */
+	purple_message_model_set_edited(model, FALSE);
+	edited_at = purple_message_model_get_edited_at(model);
+	g_assert_null(edited_at);
+
+	/* Set the edited-at property to now. */
+	purple_message_model_set_edited_at(model, now);
+	edited = purple_message_model_get_edited(model);
+	g_assert_true(edited);
+
+	/* Set the edited-at-property NULL. */
+	purple_message_model_set_edited_at(model, NULL);
+	edited = purple_message_model_get_edited(model);
+	g_assert_false(edited);
+
+	/* Cleanup. */
+	g_assert_finalize_object(model);
+	birb_date_time_clear(&now);
+}
+
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+int
+main(int argc, char *argv[]) {
+	int ret = 0;
+
+	g_test_init(&argc, &argv, NULL);
+
+	test_ui_purple_init();
+
+	g_test_add_func("/message-model/properties",
+	                test_purple_message_model_properties);
+	g_test_add_func("/message-model/computed-property/delivered",
+	                test_purple_message_model_computed_property_delivered);
+	g_test_add_func("/message-model/computed-property/edited",
+	                test_purple_message_model_computed_property_edited);
+
+	ret = g_test_run();
+
+	test_ui_purple_uninit();
+
+	return ret;
+}
diff --git a/subprojects/birb.wrap b/subprojects/birb.wrap
--- a/subprojects/birb.wrap
+++ b/subprojects/birb.wrap
@@ -1,4 +1,4 @@
 [wrap-hg]
 directory = birb
 url = https://keep.imfreedom.org/birb/birb/
-revision = 6938f7dbd994
+revision = v0.1
