An inbox between two members — conversations, per-person unread marks,
photos, blocking. Not live chat: that needs a connection held open per
signed-in member, which the sync workers cannot do.
Membership of the conversation is the whole access rule and is checked on
every hit, answering 404 rather than 403 so a member cannot tell a
conversation that is not theirs from one that does not exist. A picture
in a private message is checked the same way: on the board being signed
in is enough, here it is nowhere near.
Blocking is symmetric. One row stops both directions, and you can only
lift your own. A block that silenced only the blocked person would leave
the blocker writing freely, which is a megaphone rather than a safety
feature. Enforced in the handlers, with a test that posts from a page
held open from before the block.
Erasing a member deletes their private messages, both sides, and their
pictures off disk. A thread outlives its author because other people
replied; a two-party exchange has no remainder, and keeping half of
erased correspondence is what erasure exists to prevent. The guard added
in c9c549e did its job: it failed the moment the new tables landed and
named all four columns.
The part that needed care: schema.sql is all CREATE TABLE IF NOT EXISTS,
so it can add a table and nothing else. Every change so far happened to
be a new table. Letting an attachment belong to a message is not — and
SQLite cannot do it in place, because the table carries a CHECK
constraint and there is no DROP CONSTRAINT. Verified before building on
it: ALTER TABLE ADD COLUMN succeeds and the next insert is refused.
So migrations.py, numbered steps recorded in PRAGMA user_version, run
after the schema so a fresh database finds its work already done. Step 1
rebuilds attachments the documented way. Tested against a database built
in the old shape with rows in it, because a migration tested only on a
fresh database is tested against the one case it was never needed for —
including that the rebuilt CHECK is as strict as the one it replaced.
229 tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NizVpJ2dwzCbjCrTLCjeHn
51 lines
1.8 KiB
HTML
51 lines
1.8 KiB
HTML
{% extends "base.html" %}
|
|
{% from "_attachments.html" import attachments %}
|
|
{% block title %}{{ other.display_name if other else 'Conversación' }}{% endblock %}
|
|
|
|
{% block main %}
|
|
<div class="feed-head">
|
|
<h1>{{ other.display_name if other else 'Miembro eliminado' }}</h1>
|
|
<a class="linkish" href="{{ url_for('messages.inbox') }}">Volver</a>
|
|
</div>
|
|
|
|
{% for m in messages %}
|
|
<article class="card card--comment">
|
|
<div class="card__meta">
|
|
<span>{{ m.author }}</span> · <time>{{ m.created_at }}</time>
|
|
</div>
|
|
<div class="prose">{{ to_html(m.body_md) }}</div>
|
|
{{ attachments(images.get(m.id, [])) }}
|
|
</article>
|
|
{% endfor %}
|
|
|
|
<span id="final"></span>
|
|
|
|
{% if other is none %}
|
|
<p class="muted">Esta persona ya no está en la comunidad.</p>
|
|
{% elif blocked %}
|
|
<p class="muted">
|
|
Hay un bloqueo entre vosotros, así que ninguno de los dos puede escribir.
|
|
</p>
|
|
<form method="post" action="{{ url_for('messages.unblock', member_id=other.id) }}">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<button class="linkish" type="submit">Quitar mi bloqueo</button>
|
|
</form>
|
|
<p class="muted small">
|
|
Si el bloqueo lo puso la otra persona, esto no lo quita.
|
|
</p>
|
|
{% else %}
|
|
<form class="card" method="post" enctype="multipart/form-data"
|
|
action="{{ url_for('messages.send', conversation_id=conversation_id) }}">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<label for="body">Mensaje</label>
|
|
<textarea id="body" name="body" rows="4"
|
|
placeholder="Se puede usar Markdown: **negrita**, listas, enlaces."></textarea>
|
|
|
|
<label for="pictures">Imágenes <span class="muted small">(opcional)</span></label>
|
|
<input id="pictures" name="pictures" type="file" multiple accept="image/*">
|
|
|
|
<button class="btn" type="submit">Enviar</button>
|
|
</form>
|
|
{% endif %}
|
|
{% endblock %}
|