Implement contact matching

This commit is contained in:
2026-02-15 23:48:32 +00:00
parent 1e83c800f8
commit d9491ae817
12 changed files with 667 additions and 46 deletions

View File

@@ -0,0 +1,129 @@
{% extends "index.html" %}
{% block content %}
<section class="section">
<div class="container">
<div class="level" style="margin-bottom: 0.75rem;">
<div class="level-left">
<div>
<h1 class="title is-4" style="margin-bottom: 0.2rem;">Contact Match</h1>
<p class="is-size-7 has-text-grey">
Manually link Signal, WhatsApp, Instagram, and XMPP identifiers to people.
</p>
</div>
</div>
<div class="level-right">
<a class="button is-light" href="{% url 'compose_workspace' %}">
<span class="icon is-small"><i class="fa-solid fa-table-cells-large"></i></span>
<span>Manual Workspace</span>
</a>
</div>
</div>
{% if notice_message %}
<article class="notification is-{{ notice_level|default:'info' }} is-light">
{{ notice_message }}
</article>
{% endif %}
<div class="columns is-variable is-4">
<div class="column is-5">
<article class="box">
<h2 class="title is-6">Create Or Link Identifier</h2>
<form method="post">
{% csrf_token %}
<div class="field">
<label class="label is-small">Service</label>
<div class="select is-fullwidth">
<select name="service" required>
{% for key, label in service_choices %}
<option value="{{ key }}" {% if key == prefill_service %}selected{% endif %}>
{{ label }}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="field">
<label class="label is-small">Identifier</label>
<div class="control">
<input class="input" type="text" name="identifier" value="{{ prefill_identifier }}" required>
</div>
</div>
<div class="field">
<label class="label is-small">Existing Person</label>
<div class="select is-fullwidth">
<select name="person_id">
<option value="">- Select person -</option>
{% for person in people %}
<option value="{{ person.id }}">{{ person.name }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="field">
<label class="label is-small">Or Create Person</label>
<div class="control">
<input class="input" type="text" name="person_name" placeholder="New person name">
</div>
</div>
<button class="button is-link" type="submit">
<span class="icon is-small"><i class="fa-solid fa-link"></i></span>
<span>Save Match</span>
</button>
</form>
</article>
</div>
<div class="column is-7">
<article class="box">
<h2 class="title is-6">Discovered Contacts</h2>
{% if candidates %}
<div class="table-container">
<table class="table is-fullwidth is-hoverable is-striped">
<thead>
<tr>
<th>Contact</th>
<th>Service</th>
<th>Identifier</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
{% for row in candidates %}
<tr>
<td>{{ row.person_name }}</td>
<td>
<span class="icon is-small"><i class="{{ row.service_icon_class }}"></i></span>
{{ row.service|title }}
</td>
<td><code>{{ row.identifier }}</code></td>
<td>
{% if row.linked_person %}
<span class="tag is-success is-light">linked</span>
{% else %}
<span class="tag is-warning is-light">unlinked</span>
{% endif %}
</td>
<td>
<a class="button is-small is-light" href="{{ row.compose_url }}">
<span class="icon is-small"><i class="fa-solid fa-paper-plane"></i></span>
<span>Message</span>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="has-text-grey">No contacts discovered yet.</p>
{% endif %}
</article>
</div>
</div>
</div>
</section>
{% endblock %}

View File

@@ -64,7 +64,7 @@
<span
class="tag is-dark"
style="min-width: 2.5rem; justify-content: center;">
<i class="{{ manual_icon_class }}" aria-hidden="true"></i>
<i class="{{ row.service_icon_class|default:manual_icon_class }}" aria-hidden="true"></i>
</span>
<span
class="tag is-white"
@@ -103,6 +103,15 @@
</span>
</span>
</button>
{% if not row.linked_person %}
<a
class="button is-small is-light"
href="{{ row.match_url }}"
title="Link this identifier to a person">
<span class="icon is-small"><i class="fa-solid fa-link"></i></span>
<span>Match</span>
</a>
{% endif %}
{% endfor %}
</div>
{% else %}

View File

@@ -1,15 +1,23 @@
{% if items %}
{% for item in items %}
<a class="navbar-item" href="{{ item.compose_url }}">
<span class="icon is-small"><i class="{{ manual_icon_class }}"></i></span>
<span class="icon is-small"><i class="{{ item.service_icon_class|default:manual_icon_class }}"></i></span>
<span style="margin-left: 0.35rem;">
{{ item.person_name }} · {{ item.service|title }}
{% if not item.linked_person %}
<small class="has-text-grey"> · unlinked</small>
{% endif %}
</span>
</a>
{% endfor %}
{% else %}
<a class="navbar-item is-disabled">No contacts found.</a>
{% endif %}
<hr class="navbar-divider" style="margin: 0.2rem 0;">
<a class="navbar-item" href="{{ match_url }}">
<span class="icon is-small"><i class="fa-solid fa-link"></i></span>
<span style="margin-left: 0.35rem;">Match Contacts</span>
</a>
{% if is_preview %}
<hr class="navbar-divider" style="margin: 0.2rem 0;">
<a

View File

@@ -38,7 +38,7 @@
</button>
{% if show_contact_actions %}
{% if type == 'page' %}
<a href="{% url 'signal_contacts' type=type pk=item %}"><button
<a href="{% url contacts_url_name type=type pk=item %}"><button
class="button">
<span class="icon-text">
<span class="icon">
@@ -47,7 +47,7 @@
</span>
</button>
</a>
<a href="{% url 'signal_chats' type=type pk=item %}"><button
<a href="{% url chats_url_name type=type pk=item %}"><button
class="button">
<span class="icon-text">
<span class="icon">
@@ -59,7 +59,7 @@
{% else %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'signal_contacts' type=type pk=item %}"
hx-get="{% url contacts_url_name type=type pk=item %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"
@@ -72,7 +72,7 @@
</button>
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{% url 'signal_chats' type=type pk=item %}"
hx-get="{% url chats_url_name type=type pk=item %}"
hx-trigger="click"
hx-target="#{{ type }}s-here"
hx-swap="innerHTML"

View File

@@ -68,6 +68,16 @@
</span>
</button>
{% endif %}
<a href="{{ item.match_url }}"><button
class="button"
title="Match identifier to person">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-link"></i>
</span>
</span>
</button>
</a>
<a href="{{ item.ai_url }}"><button
class="button"
title="Open AI workspace">
@@ -102,6 +112,13 @@
</span>
</button>
{% endif %}
<a href="{{ item.match_url }}"><button class="button" title="Match identifier to person">
<span class="icon-text">
<span class="icon">
<i class="fa-solid fa-link"></i>
</span>
</span>
</button></a>
<a href="{{ item.ai_url }}"><button class="button">
<span class="icon-text">
<span class="icon">

View File

@@ -0,0 +1,57 @@
{% include 'mixins/partials/notify.html' %}
<table
class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table"
id="{{ context_object_name }}-table"
hx-swap="outerHTML"
hx-trigger="{{ context_object_name_singular }}Event from:body"
hx-get="{{ list_url }}">
<thead>
<th>chat</th>
<th>identifier</th>
<th>person</th>
<th>actions</th>
</thead>
{% for item in object_list %}
<tr>
<td>{{ item.name|default:"WhatsApp Chat" }}</td>
<td>
<a
class="has-text-grey button nowrap-child"
onclick="window.prompt('Copy to clipboard: Ctrl+C, Enter', '{{ item.identifier }}');">
<span class="icon" data-tooltip="Copy identifier">
<i class="fa-solid fa-copy" aria-hidden="true"></i>
</span>
</a>
</td>
<td>{{ item.person_name|default:"-" }}</td>
<td>
<div class="buttons">
{% if type == 'page' %}
<a href="{{ item.compose_page_url }}" class="button" title="Manual text mode">
<span class="icon"><i class="fa-solid fa-paper-plane"></i></span>
</a>
{% else %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{{ item.compose_widget_url }}"
hx-trigger="click"
hx-target="#widgets-here"
hx-swap="afterend"
class="button"
title="Manual text mode widget">
<span class="icon"><i class="fa-solid fa-paper-plane"></i></span>
</button>
{% endif %}
<a href="{{ item.match_url }}" class="button" title="Match identifier">
<span class="icon"><i class="fa-solid fa-link"></i></span>
</a>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="has-text-grey">No WhatsApp chats discovered yet.</td>
</tr>
{% endfor %}
</table>

View File

@@ -0,0 +1,53 @@
{% include 'mixins/partials/notify.html' %}
<table
class="table is-fullwidth is-hoverable"
hx-target="#{{ context_object_name }}-table"
id="{{ context_object_name }}-table"
hx-swap="outerHTML"
hx-trigger="{{ context_object_name_singular }}Event from:body"
hx-get="{{ list_url }}">
<thead>
<th>name</th>
<th>identifier</th>
<th>jid</th>
<th>person</th>
<th>actions</th>
</thead>
{% for item in object_list %}
<tr>
<td>{{ item.name|default:"-" }}</td>
<td>
<code>{{ item.identifier }}</code>
</td>
<td>{{ item.jid|default:"-" }}</td>
<td>{{ item.person_name|default:"-" }}</td>
<td>
<div class="buttons">
{% if type == 'page' %}
<a href="{{ item.compose_page_url }}" class="button" title="Open manual chat">
<span class="icon"><i class="fa-solid fa-paper-plane"></i></span>
</a>
{% else %}
<button
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-get="{{ item.compose_widget_url }}"
hx-trigger="click"
hx-target="#widgets-here"
hx-swap="afterend"
class="button"
title="Open manual chat widget">
<span class="icon"><i class="fa-solid fa-paper-plane"></i></span>
</button>
{% endif %}
<a href="{{ item.match_url }}" class="button" title="Match identifier">
<span class="icon"><i class="fa-solid fa-link"></i></span>
</a>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="has-text-grey">No WhatsApp contacts discovered yet.</td>
</tr>
{% endfor %}
</table>