| 
 | 1 | +defmodule AlgoraWeb.Chat.ThreadLive do  | 
 | 2 | +  @moduledoc false  | 
 | 3 | +  use AlgoraWeb, :live_view  | 
 | 4 | + | 
 | 5 | +  alias Algora.Chat  | 
 | 6 | +  alias Algora.Chat.Message  | 
 | 7 | +  alias Algora.Repo  | 
 | 8 | + | 
 | 9 | +  @impl true  | 
 | 10 | +  def mount(%{"id" => thread_id}, _session, socket) do  | 
 | 11 | +    if connected?(socket) do  | 
 | 12 | +      Chat.subscribe(thread_id)  | 
 | 13 | +    end  | 
 | 14 | + | 
 | 15 | +    thread = thread_id |> Chat.get_thread() |> Repo.preload(participants: :user)  | 
 | 16 | +    messages = thread_id |> Chat.list_messages() |> Repo.preload(:sender)  | 
 | 17 | + | 
 | 18 | +    {:ok,  | 
 | 19 | +     socket  | 
 | 20 | +     |> assign(:thread, thread)  | 
 | 21 | +     |> assign(:thread_id, thread_id)  | 
 | 22 | +     |> assign(:messages, messages)}  | 
 | 23 | +  end  | 
 | 24 | + | 
 | 25 | +  @impl true  | 
 | 26 | +  def handle_event("send_message", %{"message" => content}, socket) do  | 
 | 27 | +    {:ok, message} =  | 
 | 28 | +      Chat.send_message(  | 
 | 29 | +        socket.assigns.thread_id,  | 
 | 30 | +        socket.assigns.current_user.id,  | 
 | 31 | +        content  | 
 | 32 | +      )  | 
 | 33 | + | 
 | 34 | +    {:noreply, push_event(socket, "clear-input", %{selector: "#message-input"})}  | 
 | 35 | +  end  | 
 | 36 | + | 
 | 37 | +  @impl true  | 
 | 38 | +  def handle_info(%Message{} = message, socket) do  | 
 | 39 | +    if message.id in Enum.map(socket.assigns.messages, & &1.id) do  | 
 | 40 | +      {:noreply, socket}  | 
 | 41 | +    else  | 
 | 42 | +      {:noreply, assign(socket, :messages, socket.assigns.messages ++ [message])}  | 
 | 43 | +    end  | 
 | 44 | +  end  | 
 | 45 | + | 
 | 46 | +  @impl true  | 
 | 47 | +  def render(assigns) do  | 
 | 48 | +    ~H"""  | 
 | 49 | +    <div class="pr-80">  | 
 | 50 | +      <div class="flex flex-col h-[calc(100vh-4rem)]">  | 
 | 51 | +        <div class="flex-none border-b border-border bg-card/50 p-4 backdrop-blur supports-[backdrop-filter]:bg-background/60">  | 
 | 52 | +          <div class="flex items-center justify-between">  | 
 | 53 | +            <div class="flex items-center gap-3">  | 
 | 54 | +              <div class="flex -space-x-2">  | 
 | 55 | +                <%= for participant <- @thread.participants do %>  | 
 | 56 | +                  <.avatar class="relative z-10 h-8 w-8 ring-2 ring-background">  | 
 | 57 | +                    <.avatar_image src={participant.user.avatar_url} alt={participant.user.name} />  | 
 | 58 | +                    <.avatar_fallback>  | 
 | 59 | +                      {Algora.Util.initials(participant.user.name)}  | 
 | 60 | +                    </.avatar_fallback>  | 
 | 61 | +                  </.avatar>  | 
 | 62 | +                <% end %>  | 
 | 63 | +              </div>  | 
 | 64 | +              <div>  | 
 | 65 | +                <h2 class="text-lg font-semibold">{@thread.title}</h2>  | 
 | 66 | +                <p class="text-xs text-muted-foreground">  | 
 | 67 | +                  {@thread.participants  | 
 | 68 | +                  |> Enum.map(& &1.user.name)  | 
 | 69 | +                  |> Algora.Util.format_name_list()}  | 
 | 70 | +                </p>  | 
 | 71 | +              </div>  | 
 | 72 | +            </div>  | 
 | 73 | +          </div>  | 
 | 74 | +        </div>  | 
 | 75 | +
  | 
 | 76 | +        <.scroll_area  | 
 | 77 | +          class="flex flex-1 flex-col-reverse gap-6 p-4"  | 
 | 78 | +          id="messages-container"  | 
 | 79 | +          phx-hook="ScrollToBottom"  | 
 | 80 | +        >  | 
 | 81 | +          <div class="space-y-6">  | 
 | 82 | +            <%= for {date, messages} <- @messages  | 
 | 83 | +                |> Enum.group_by(fn msg ->  | 
 | 84 | +                  case Date.diff(Date.utc_today(), DateTime.to_date(msg.inserted_at)) do  | 
 | 85 | +                    0 -> "Today"  | 
 | 86 | +                    1 -> "Yesterday"  | 
 | 87 | +                    n when n <= 7 -> Calendar.strftime(msg.inserted_at, "%A")  | 
 | 88 | +                    _ -> Calendar.strftime(msg.inserted_at, "%b %d")  | 
 | 89 | +                  end  | 
 | 90 | +                end)  | 
 | 91 | +                |> Enum.sort_by(fn {_, msgs} -> hd(msgs).inserted_at end, Date) do %>  | 
 | 92 | +              <div class="flex items-center justify-center">  | 
 | 93 | +                <div class="rounded-full bg-background px-2 py-1 text-xs text-muted-foreground">  | 
 | 94 | +                  {date}  | 
 | 95 | +                </div>  | 
 | 96 | +              </div>  | 
 | 97 | +
  | 
 | 98 | +              <div class="flex flex-col gap-6">  | 
 | 99 | +                <%= for message <- Enum.sort_by(messages, & &1.inserted_at, Date) do %>  | 
 | 100 | +                  <div class="group flex gap-3">  | 
 | 101 | +                    <.avatar class="h-8 w-8">  | 
 | 102 | +                      <.avatar_image src={message.sender.avatar_url} />  | 
 | 103 | +                      <.avatar_fallback>  | 
 | 104 | +                        {Algora.Util.initials(message.sender.name)}  | 
 | 105 | +                      </.avatar_fallback>  | 
 | 106 | +                    </.avatar>  | 
 | 107 | +                    <div class="max-w-[80%] relative rounded-2xl rounded-tl-none bg-muted p-3">  | 
 | 108 | +                      {message.content}  | 
 | 109 | +                      <div class="text-[10px] mt-1 text-muted-foreground">  | 
 | 110 | +                        {message.inserted_at  | 
 | 111 | +                        |> DateTime.to_time()  | 
 | 112 | +                        |> Time.to_string()  | 
 | 113 | +                        |> String.slice(0..4)}  | 
 | 114 | +                      </div>  | 
 | 115 | +                    </div>  | 
 | 116 | +                  </div>  | 
 | 117 | +                <% end %>  | 
 | 118 | +              </div>  | 
 | 119 | +            <% end %>  | 
 | 120 | +          </div>  | 
 | 121 | +        </.scroll_area>  | 
 | 122 | +
  | 
 | 123 | +        <div class="flex-none bg-card/50 p-4 backdrop-blur supports-[backdrop-filter]:bg-background/60">  | 
 | 124 | +          <form phx-submit="send_message" class="flex items-center gap-2">  | 
 | 125 | +            <div class="relative flex-1">  | 
 | 126 | +              <.input  | 
 | 127 | +                id="message-input"  | 
 | 128 | +                type="text"  | 
 | 129 | +                name="message"  | 
 | 130 | +                value=""  | 
 | 131 | +                placeholder="Type a message..."  | 
 | 132 | +                autocomplete="off"  | 
 | 133 | +                class="flex-1 pr-24"  | 
 | 134 | +                phx-hook="ClearInput"  | 
 | 135 | +              />  | 
 | 136 | +              <div class="absolute top-1/2 right-2 flex -translate-y-1/2 gap-1">  | 
 | 137 | +                <.button  | 
 | 138 | +                  type="button"  | 
 | 139 | +                  variant="ghost"  | 
 | 140 | +                  size="icon-sm"  | 
 | 141 | +                  phx-hook="EmojiPicker"  | 
 | 142 | +                  id="emoji-trigger"  | 
 | 143 | +                >  | 
 | 144 | +                  <.icon name="tabler-mood-smile" class="h-4 w-4" />  | 
 | 145 | +                </.button>  | 
 | 146 | +              </div>  | 
 | 147 | +            </div>  | 
 | 148 | +            <.button type="submit" size="icon">  | 
 | 149 | +              <.icon name="tabler-send" class="h-4 w-4" />  | 
 | 150 | +            </.button>  | 
 | 151 | +          </form>  | 
 | 152 | +          <div id="emoji-picker-container" class="bottom-[80px] absolute right-4 hidden">  | 
 | 153 | +            <emoji-picker></emoji-picker>  | 
 | 154 | +          </div>  | 
 | 155 | +        </div>  | 
 | 156 | +      </div>  | 
 | 157 | +
  | 
 | 158 | +      <aside class="fixed top-[4rem] right-0 z-20 h-full w-72 border-l border-border bg-card/50 p-6 backdrop-blur supports-[backdrop-filter]:bg-background/60">  | 
 | 159 | +        <div class="space-y-6">  | 
 | 160 | +          <div>  | 
 | 161 | +            <h3 class="mb-4 text-sm font-medium">Participants</h3>  | 
 | 162 | +            <div class="space-y-4">  | 
 | 163 | +              <%= for participant <- @thread.participants do %>  | 
 | 164 | +                <div class="flex items-center gap-3">  | 
 | 165 | +                  <.avatar class="h-10 w-10">  | 
 | 166 | +                    <.avatar_image src={participant.user.avatar_url} alt={participant.user.name} />  | 
 | 167 | +                    <.avatar_fallback>  | 
 | 168 | +                      {Algora.Util.initials(participant.user.name)}  | 
 | 169 | +                    </.avatar_fallback>  | 
 | 170 | +                  </.avatar>  | 
 | 171 | +                  <div>  | 
 | 172 | +                    <p class="text-sm font-medium leading-none">{participant.user.name}</p>  | 
 | 173 | +                    <p :if={participant.user.last_active_at} class="text-xs text-muted-foreground">  | 
 | 174 | +                      Active {Algora.Util.time_ago(participant.user.last_active_at)}  | 
 | 175 | +                    </p>  | 
 | 176 | +                  </div>  | 
 | 177 | +                </div>  | 
 | 178 | +              <% end %>  | 
 | 179 | +            </div>  | 
 | 180 | +          </div>  | 
 | 181 | +        </div>  | 
 | 182 | +      </aside>  | 
 | 183 | +    </div>  | 
 | 184 | +    """  | 
 | 185 | +  end  | 
 | 186 | +end  | 
0 commit comments