What's the best way to create a calculated field, or add a field when insterting, based on the previously inserted row

Usually in a chat app, if you post a message/comment/etc. then the next message/comment/etc. is from the same user, and posted in under X minutes, it’s displayed is a “continued” message/comment/etc. (doesn’t re-display the username/avatar/etc.).

I have figured out how to do this via a view helper, but it makes the markup clunky as I always need to carry around the previous_message instead of just having the current message. What I’d like to do is have this stored as an attribute on the message, or as a calculated field when loading a message/list of messages from the DB.

One way I thought to do it is via something in the changeset like this:

def changeset(comment, attrs) do
  |> add_is_continued_comment()

def add_is_continued_comment(changeset) do
  post_id = get_field(changeset, :post_id)
  user_id = get_field(changeset, :user_id)

  prev_cmt =
    from(p in __MODULE__, where: p.post_id == ^post_id, order_by: [desc: :inserted_at], limit: 1)
    |> Repo.one()
  diff = Timex.diff(DateTime.utc_now(), prev_cmt.inserted_at) / 1_000_000
  put_change(changeset, :is_continued_comment, prev_cmt.user_id == user_id && diff < 60 * 2)

But this seems like it’s likely to cause race conditions. Another way it to make it virtual field, and populate it when getting the rows out of the database, but this seems clunky too. The other thought is to do it via a Postgresql Trigger, and have the DB calculate it on insert (no race conditions, faster, and no need to calculate when retrieving rows).

Is this a pattern you’ve dealt with before? What’s the best approach?

I’d proceed with a view helper. Could you share current implementation?

Here’s the current implementation. I’d like to move to using LiveComponents for each message, and slimming down the assigns needed will simplify the logic. I’m thinking a DB trigger is the fastest/cleanest way to do it.

<%= for {message, index} <- Enum.with_index(@messages) do %>
        <%= if index == 0 do %>
          <%= render "_message.html", Map.put(assigns, :message, message) %>
        <% else %>
          <%= if continue_message?(Enum.at(@messages, index - 1), message) do %>
            <%= render "_message_cont.html", Map.put(assigns, :message, message) %>
          <% else %>
            <%= render "_message.html", Map.put(assigns, :message, message) %>
          <% end %>
        <% end %>
      <% end %>

Give your messages something unique by sender, which you can target via css, and with selector + selector target the ones following up after each other to alternate the styling. I‘d expect this styling only version to be better for accessibility as well, as e.g. blind/bad-sighted people likely need the full markup per message.

1 Like

Enum.at in a loop is not an optimal solution for lists.
Usually, I implement something like this:

def map_messages(messages, mapper) when is_function(mapper, 2) do
  |> Enum.map_reduce(nil, fn message, prev_message ->
    continue_message? = not is_nil(prev_message) and continue_message?(message, prev_message)
    {mapper.(message, continue_message?), message}
  |> elem(0)
map_messages(@messages, fn
  message, true -> render "_message_cont.html", Map.put(assigns, :message, message)
  message, false -> render "_message.html", Map.put(assigns, :message, message)

Does it solve your issue for LV?