diff --git a/priv/assets/css/ldf.css b/priv/assets/css/ldf.css
index b576df3..5de2ef8 100644
--- a/priv/assets/css/ldf.css
+++ b/priv/assets/css/ldf.css
@@ -134,6 +134,22 @@ tbody tr:last-child td { border-bottom: none; }
}
tbody tr.new { animation: flashin .7s ease-out; }
+/* intercept records (receiver feed) */
+.records { display: flex; flex-direction: column; }
+.rec { padding: 14px 18px; border-bottom: 1px solid var(--line-soft); }
+.rec:last-child { border-bottom: none; }
+.rec.new { animation: flashin .7s ease-out; }
+.rec-head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 8px; }
+.rec-from { font-family: var(--sans); font-weight: 600; font-size: 13px; color: var(--text); }
+.rec-phone { color: var(--amber); font-weight: 400; font-size: 12px; margin-left: 8px; }
+.rec-time { margin-left: auto; color: var(--faint); font-size: 11px; white-space: nowrap; }
+.rec-body { color: var(--text); font-size: 13px; margin-bottom: 10px; word-break: break-word; padding-left: 10px; border-left: 2px solid var(--line); }
+.rec-body a { color: var(--live); text-decoration: none; border-bottom: 1px dotted var(--live-dim); word-break: break-all; }
+.rec-body a:hover { border-bottom-style: solid; }
+.rec-meta { display: flex; flex-wrap: wrap; gap: 5px 18px; font-size: 10.5px; color: var(--dim); }
+.rec-meta i { color: var(--faint); font-style: normal; text-transform: uppercase; letter-spacing: .08em; margin-right: 6px; }
+.rec-meta .rec-agent { flex-basis: 100%; color: var(--faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+
.empty { padding: 40px 18px; text-align: center; color: var(--faint); font-size: 12.5px; }
.empty::before { content: "▱ "; color: var(--live-dim); }
diff --git a/src/controllers/ldf_www_controller.erl b/src/controllers/ldf_www_controller.erl
index f527ab9..068bfcf 100644
--- a/src/controllers/ldf_www_controller.erl
+++ b/src/controllers/ldf_www_controller.erl
@@ -77,8 +77,8 @@ submit_history(Req0) ->
%% --- published by the message API on each intercepted message -------------
-publish_message(MessageId, Payload) ->
- Row = render(message_row_dtl, row_vars(ref(MessageId), Payload, true)),
+publish_message(_MessageId, Payload) ->
+ Row = render(message_row_dtl, row_vars(Payload, true)),
datastar_nova:publish(?MESSAGES, [
datastar:patch_elements(~"", #{selector => ~"#feed-empty", mode => remove}),
datastar:patch_elements(Row, #{selector => ~"#messages", mode => append})
@@ -91,35 +91,75 @@ publish_message(MessageId, Payload) ->
message_init([]) ->
[datastar:patch_elements(feed_empty(), #{selector => ~"#messages", mode => inner})];
message_init(Msgs) ->
- Rows = [
- render(
- message_row_dtl,
- row_vars(ref(maps:get(message_id, M, ~"")), maps:get(payload, M, ~""), false)
- )
- || M <- Msgs
- ],
+ Rows = [render(message_row_dtl, row_vars(maps:get(payload, M, ~""), false)) || M <- Msgs],
[datastar:patch_elements(Rows, #{selector => ~"#messages", mode => inner})].
feed_empty() ->
- ~"
| Awaiting interception |
".
-
-%% The stored payload is the full encoded message; show its inner payload -
-%% an attachment URL becomes a clickable link, plain text stays text.
-row_vars(Ref, StoredPayload, New) ->
- case display_payload(StoredPayload) of
- {link, Url} -> [{ref, Ref}, {new, New}, {link, Url}, {text, ~""}];
- {text, Text} -> [{ref, Ref}, {new, New}, {link, false}, {text, Text}]
+ ~"Awaiting interception
".
+
+%% The stored payload is the full encoded message; expand every field for a
+%% structured record. The inner payload renders as text, or a clickable link
+%% for attachments.
+row_vars(StoredPayload, New) ->
+ case safe_decode(StoredPayload) of
+ Msg when is_map(Msg) ->
+ [{new, New} | fields(Msg)];
+ _ ->
+ [{new, New}, {text, StoredPayload}, {link, false} | blank_fields()]
end.
-display_payload(StoredPayload) ->
- try json:decode(StoredPayload) of
- #{~"payload" := #{~"url" := Url}} when is_binary(Url) -> {link, public_url(Url)};
- #{~"payload" := Text} when is_binary(Text) -> {text, Text};
- _ -> {text, StoredPayload}
+fields(Msg) ->
+ SenderInfo = maps:get(~"sender_info", Msg, #{}),
+ Payload =
+ case maps:get(~"payload", Msg, ~"") of
+ #{~"url" := Url} when is_binary(Url) -> [{link, public_url(Url)}, {text, ~""}];
+ Text when is_binary(Text) -> [{link, false}, {text, Text}];
+ _ -> [{link, false}, {text, ~""}]
+ end,
+ [
+ {email, maps:get(~"email", SenderInfo, ~"")},
+ {phone, maps:get(~"phone_number", SenderInfo, ~"")},
+ {agent, maps:get(~"user_agent", SenderInfo, ~"")},
+ {time, fmt_time(maps:get(~"timestamp", Msg, undefined))},
+ {type, maps:get(~"type", Msg, ~"")},
+ {action, maps:get(~"action", Msg, ~"")},
+ {sender, short(maps:get(~"sender", Msg, ~""))},
+ {recipient, short(maps:get(~"to", Msg, ~""))},
+ {chat, short(maps:get(~"chat_id", Msg, ~""))},
+ {id, short(maps:get(~"id", Msg, ~""))}
+ | Payload
+ ].
+
+blank_fields() ->
+ [
+ {email, ~""},
+ {phone, ~""},
+ {agent, ~""},
+ {time, ~""},
+ {type, ~""},
+ {action, ~""},
+ {sender, ~""},
+ {recipient, ~""},
+ {chat, ~""},
+ {id, ~""}
+ ].
+
+safe_decode(Bin) ->
+ try json:decode(Bin) of
+ Decoded -> Decoded
catch
- _:_ -> {text, StoredPayload}
+ _:_ -> error
end.
+fmt_time(Ms) when is_integer(Ms) ->
+ list_to_binary(calendar:system_time_to_rfc3339(Ms div 1000, [{offset, "Z"}]));
+fmt_time(_) ->
+ ~"".
+
+short(<>) -> Eight;
+short(Bin) when is_binary(Bin) -> Bin;
+short(_) -> ~"".
+
%% Rewrite an attachment URL onto the browser-reachable chatli host,
%% regardless of which host got baked in when it was stored.
public_url(Url) ->
@@ -155,7 +195,3 @@ status_patch(Text) ->
render(Mod, Vars) ->
{ok, Html} = Mod:render(Vars),
string:trim(iolist_to_binary(Html), trailing).
-
-ref(<>) -> Short;
-ref(MessageId) when is_binary(MessageId) -> MessageId;
-ref(_) -> ~"".
diff --git a/src/views/message_row.dtl b/src/views/message_row.dtl
index 73c29b1..4a1353f 100644
--- a/src/views/message_row.dtl
+++ b/src/views/message_row.dtl
@@ -1 +1 @@
-| {{ ref }} | {% if link %}{{ link }}{% else %}{{ text }}{% endif %} |
+{% if email %}{{ email }}{% endif %}{% if phone %}{{ phone }}{% endif %}{% if not email %}{% if not phone %}unidentified{% endif %}{% endif %}{{ time }}
{% if link %}
{{ link }}{% else %}{{ text }}{% endif %}
type{{ type }}{% if action %} / {{ action }}{% endif %}msg{{ id }}chat{{ chat }}sender{{ sender }}to{{ recipient }}{% if agent %}agent{{ agent }}{% endif %}
diff --git a/src/views/receiver.dtl b/src/views/receiver.dtl
index 9593a41..cfbd11c 100644
--- a/src/views/receiver.dtl
+++ b/src/views/receiver.dtl
@@ -10,12 +10,9 @@
SSE · /sse/receiver
-
- | Reference | Payload |
-
- | Awaiting interception |
-
-
+
+
Awaiting interception
+
{% endblock %}