mirror of
https://github.com/algora-io/tv.git
synced 2025-04-07 06:49:52 +02:00
rewrite chat with liveview streams & pubsub events (#31)
This commit is contained in:
parent
12e17848f4
commit
cc1c5ddc4d
@ -1,7 +1,6 @@
|
|||||||
import "phoenix_html";
|
import "phoenix_html";
|
||||||
import { Socket } from "phoenix";
|
import { Socket } from "phoenix";
|
||||||
import { LiveSocket, type ViewHook } from "phoenix_live_view";
|
import { LiveSocket, type ViewHook } from "phoenix_live_view";
|
||||||
import Chat from "./user_socket";
|
|
||||||
import topbar from "../vendor/topbar";
|
import topbar from "../vendor/topbar";
|
||||||
import videojs from "../vendor/video";
|
import videojs from "../vendor/video";
|
||||||
import "../vendor/videojs-youtube";
|
import "../vendor/videojs-youtube";
|
||||||
@ -10,6 +9,27 @@ import "../vendor/videojs-youtube";
|
|||||||
// TODO: enable strict mode
|
// TODO: enable strict mode
|
||||||
// TODO: eliminate anys
|
// TODO: eliminate anys
|
||||||
|
|
||||||
|
interface PhxEvent extends Event {
|
||||||
|
target: Element;
|
||||||
|
detail: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PhxEventKey = `js:${string}` | `phx:${string}`;
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
liveSocket: LiveSocket;
|
||||||
|
addEventListener<K extends keyof WindowEventMap | PhxEventKey>(
|
||||||
|
type: K,
|
||||||
|
listener: (
|
||||||
|
this: Window,
|
||||||
|
ev: K extends keyof WindowEventMap ? WindowEventMap[K] : PhxEvent
|
||||||
|
) => any,
|
||||||
|
options?: boolean | AddEventListenerOptions | undefined
|
||||||
|
): void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let isVisible = (el) =>
|
let isVisible = (el) =>
|
||||||
!!(el.offsetWidth || el.offsetHeight || el.getClientRects().length > 0);
|
!!(el.offsetWidth || el.offsetHeight || el.getClientRects().length > 0);
|
||||||
|
|
||||||
@ -163,10 +183,20 @@ const Hooks = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.handleEvent("play_video", playVideo);
|
this.handleEvent("play_video", playVideo);
|
||||||
this.handleEvent("join_chat", Chat.join);
|
},
|
||||||
this.handleEvent("message_deleted", ({ id }) => {
|
},
|
||||||
document.querySelector(`#message-${id}`)?.remove();
|
Chat: {
|
||||||
});
|
mounted() {
|
||||||
|
this.el.scrollTo(0, this.el.scrollHeight);
|
||||||
|
},
|
||||||
|
|
||||||
|
updated() {
|
||||||
|
const pixelsBelowBottom =
|
||||||
|
this.el.scrollHeight - this.el.clientHeight - this.el.scrollTop;
|
||||||
|
|
||||||
|
if (pixelsBelowBottom < 200) {
|
||||||
|
this.el.scrollTo(0, this.el.scrollHeight);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
NavBar: {
|
NavBar: {
|
||||||
|
25
assets/js/global.d.ts
vendored
25
assets/js/global.d.ts
vendored
@ -1,25 +0,0 @@
|
|||||||
import { type Channel } from "phoenix";
|
|
||||||
import { type LiveSocket } from "phoenix_live_view";
|
|
||||||
|
|
||||||
interface PhxEvent extends Event {
|
|
||||||
target: Element;
|
|
||||||
detail: Record<string, any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
type PhxEventKey = `js:${string}` | `phx:${string}`;
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
liveSocket: LiveSocket;
|
|
||||||
userToken?: string;
|
|
||||||
channel?: Channel;
|
|
||||||
addEventListener<K extends keyof WindowEventMap | PhxEventKey>(
|
|
||||||
type: K,
|
|
||||||
listener: (
|
|
||||||
this: Window,
|
|
||||||
ev: K extends keyof WindowEventMap ? WindowEventMap[K] : PhxEvent
|
|
||||||
) => any,
|
|
||||||
options?: boolean | AddEventListenerOptions | undefined
|
|
||||||
): void;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
import { type Channel, Socket } from "phoenix";
|
|
||||||
|
|
||||||
const systemUser = (sender) => sender === "algora";
|
|
||||||
|
|
||||||
const init = () => {
|
|
||||||
let socket = new Socket("/socket", { params: { token: window.userToken } });
|
|
||||||
socket.connect();
|
|
||||||
|
|
||||||
let channel: Channel;
|
|
||||||
let chatInput;
|
|
||||||
let chatMessages;
|
|
||||||
let handleSend;
|
|
||||||
|
|
||||||
const leave = (channel: Channel) => {
|
|
||||||
channel.leave();
|
|
||||||
if (chatInput) {
|
|
||||||
chatInput.value = "";
|
|
||||||
chatInput.removeEventListener("keypress", handleSend);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const join = ({ id }) => {
|
|
||||||
if (channel) {
|
|
||||||
leave(channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
channel = socket.channel(`room:${id}`, {});
|
|
||||||
chatInput = document.querySelector("#chat-input");
|
|
||||||
chatMessages = document.querySelector("#chat-messages");
|
|
||||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
||||||
|
|
||||||
handleSend = (event) => {
|
|
||||||
if (event.key === "Enter" && chatInput.value.trim()) {
|
|
||||||
channel.push("new_msg", { body: chatInput.value });
|
|
||||||
chatInput.value = "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (chatInput) {
|
|
||||||
chatInput.addEventListener("keypress", handleSend);
|
|
||||||
}
|
|
||||||
|
|
||||||
channel.on("new_msg", (payload) => {
|
|
||||||
const messageItem = document.createElement("div");
|
|
||||||
messageItem.id = `message-${payload.id}`;
|
|
||||||
messageItem.className = "group hover:bg-white/5 relative px-4";
|
|
||||||
|
|
||||||
const senderItem = document.createElement("span");
|
|
||||||
senderItem.innerText = `${payload.user.handle}: `;
|
|
||||||
senderItem.className = `font-semibold ${
|
|
||||||
systemUser(payload.user.handle) ? "text-emerald-400" : "text-indigo-400"
|
|
||||||
}`;
|
|
||||||
|
|
||||||
const bodyItem = document.createElement("span");
|
|
||||||
bodyItem.innerText = `${payload.body}`;
|
|
||||||
bodyItem.className = "font-medium text-gray-100";
|
|
||||||
|
|
||||||
messageItem.appendChild(senderItem);
|
|
||||||
messageItem.appendChild(bodyItem);
|
|
||||||
|
|
||||||
chatMessages.appendChild(messageItem);
|
|
||||||
chatMessages.scrollTop = chatMessages.scrollHeight;
|
|
||||||
});
|
|
||||||
|
|
||||||
channel
|
|
||||||
.join()
|
|
||||||
.receive("ok", (resp) => {
|
|
||||||
console.log("Joined successfully", resp);
|
|
||||||
window.channel = channel;
|
|
||||||
})
|
|
||||||
.receive("error", (resp) => {
|
|
||||||
console.log("Unable to join", resp);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return { join };
|
|
||||||
};
|
|
||||||
|
|
||||||
const Chat = init();
|
|
||||||
|
|
||||||
export default Chat;
|
|
@ -8,7 +8,13 @@ defmodule Algora.Chat do
|
|||||||
alias Algora.Accounts.User
|
alias Algora.Accounts.User
|
||||||
alias Algora.{Repo, Accounts}
|
alias Algora.{Repo, Accounts}
|
||||||
|
|
||||||
alias Algora.Chat.Message
|
alias Algora.Chat.{Message, Events}
|
||||||
|
|
||||||
|
@pubsub Algora.PubSub
|
||||||
|
|
||||||
|
def subscribe_to_room(%Video{} = video) do
|
||||||
|
Phoenix.PubSub.subscribe(@pubsub, topic(video.id))
|
||||||
|
end
|
||||||
|
|
||||||
def list_messages(%Video{} = video) do
|
def list_messages(%Video{} = video) do
|
||||||
# TODO: add limit
|
# TODO: add limit
|
||||||
@ -48,9 +54,11 @@ defmodule Algora.Chat do
|
|||||||
|> Repo.one!()
|
|> Repo.one!()
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_message(attrs \\ %{}) do
|
def create_message(%User{} = user, %Video{} = video, attrs \\ %{}) do
|
||||||
%Message{}
|
%Message{}
|
||||||
|> Message.changeset(attrs)
|
|> Message.changeset(attrs)
|
||||||
|
|> Message.put_user(user)
|
||||||
|
|> Message.put_video(video)
|
||||||
|> Repo.insert()
|
|> Repo.insert()
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -67,4 +75,18 @@ defmodule Algora.Chat do
|
|||||||
def change_message(%Message{} = message, attrs \\ %{}) do
|
def change_message(%Message{} = message, attrs \\ %{}) do
|
||||||
Message.changeset(message, attrs)
|
Message.changeset(message, attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp topic(video_id) when is_integer(video_id), do: "room:#{video_id}"
|
||||||
|
|
||||||
|
defp broadcast!(topic, msg) do
|
||||||
|
Phoenix.PubSub.broadcast!(@pubsub, topic, {__MODULE__, msg})
|
||||||
|
end
|
||||||
|
|
||||||
|
def broadcast_message_deleted!(message) do
|
||||||
|
broadcast!(topic(message.video_id), %Events.MessageDeleted{message: message})
|
||||||
|
end
|
||||||
|
|
||||||
|
def broadcast_message_sent!(message) do
|
||||||
|
broadcast!(topic(message.video_id), %Events.MessageSent{message: message})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
9
lib/algora/chat/events.ex
Normal file
9
lib/algora/chat/events.ex
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
defmodule Algora.Chat.Events do
|
||||||
|
defmodule MessageSent do
|
||||||
|
defstruct message: nil
|
||||||
|
end
|
||||||
|
|
||||||
|
defmodule MessageDeleted do
|
||||||
|
defstruct message: nil
|
||||||
|
end
|
||||||
|
end
|
@ -1,15 +1,15 @@
|
|||||||
defmodule Algora.Chat.Message do
|
defmodule Algora.Chat.Message do
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
alias Algora.Accounts
|
alias Algora.Accounts.User
|
||||||
alias Algora.Library
|
alias Algora.Library.Video
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
|
||||||
schema "messages" do
|
schema "messages" do
|
||||||
field :body, :string
|
field :body, :string
|
||||||
field :sender_handle, :string, virtual: true
|
field :sender_handle, :string, virtual: true
|
||||||
field :channel_id, :integer, virtual: true
|
field :channel_id, :integer, virtual: true
|
||||||
belongs_to :user, Accounts.User
|
belongs_to :user, User
|
||||||
belongs_to :video, Library.Video
|
belongs_to :video, Video
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
@ -20,4 +20,12 @@ defmodule Algora.Chat.Message do
|
|||||||
|> cast(attrs, [:body])
|
|> cast(attrs, [:body])
|
||||||
|> validate_required([:body])
|
|> validate_required([:body])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def put_user(%Ecto.Changeset{} = changeset, %User{} = user) do
|
||||||
|
put_assoc(changeset, :user, user)
|
||||||
|
end
|
||||||
|
|
||||||
|
def put_video(%Ecto.Changeset{} = changeset, %Video{} = video) do
|
||||||
|
put_assoc(changeset, :video, video)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -640,10 +640,6 @@ defmodule Algora.Library do
|
|||||||
Phoenix.PubSub.broadcast!(@pubsub, topic, {__MODULE__, msg})
|
Phoenix.PubSub.broadcast!(@pubsub, topic, {__MODULE__, msg})
|
||||||
end
|
end
|
||||||
|
|
||||||
def broadcast_message_deleted!(%Channel{} = channel, message) do
|
|
||||||
broadcast!(topic(channel.user_id), %Events.MessageDeleted{message: message})
|
|
||||||
end
|
|
||||||
|
|
||||||
def broadcast_processing_progressed!(stage, video, pct) do
|
def broadcast_processing_progressed!(stage, video, pct) do
|
||||||
broadcast!(topic_studio(), %Events.ProcessingProgressed{video: video, stage: stage, pct: pct})
|
broadcast!(topic_studio(), %Events.ProcessingProgressed{video: video, stage: stage, pct: pct})
|
||||||
end
|
end
|
||||||
|
@ -26,8 +26,4 @@ defmodule Algora.Library.Events do
|
|||||||
defmodule ProcessingFailed do
|
defmodule ProcessingFailed do
|
||||||
defstruct video: nil, attempt: nil, max_attempts: nil
|
defstruct video: nil, attempt: nil, max_attempts: nil
|
||||||
end
|
end
|
||||||
|
|
||||||
defmodule MessageDeleted do
|
|
||||||
defstruct message: nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
defmodule AlgoraWeb.RoomChannel do
|
|
||||||
alias Algora.Chat.Message
|
|
||||||
alias Algora.Repo
|
|
||||||
|
|
||||||
use Phoenix.Channel
|
|
||||||
|
|
||||||
def join("room:" <> _room_id, _params, socket) do
|
|
||||||
{:ok, socket}
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_in("new_msg", %{"body" => body}, socket) do
|
|
||||||
user = socket.assigns.user
|
|
||||||
"room:" <> video_id = socket.topic
|
|
||||||
|
|
||||||
if user do
|
|
||||||
message =
|
|
||||||
Repo.insert!(%Message{
|
|
||||||
body: body,
|
|
||||||
user_id: user.id,
|
|
||||||
video_id: String.to_integer(video_id)
|
|
||||||
})
|
|
||||||
|
|
||||||
broadcast!(socket, "new_msg", %{
|
|
||||||
user: %{id: user.id, handle: user.handle},
|
|
||||||
id: message.id,
|
|
||||||
body: body
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
{:noreply, socket}
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,35 +0,0 @@
|
|||||||
defmodule AlgoraWeb.UserSocket do
|
|
||||||
use Phoenix.Socket
|
|
||||||
alias Algora.Accounts
|
|
||||||
|
|
||||||
channel "room:*", AlgoraWeb.RoomChannel
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def connect(%{"token" => token}, socket, _connect_info) do
|
|
||||||
# max_age: 1209600 is equivalent to two weeks in seconds
|
|
||||||
case Phoenix.Token.verify(socket, "user socket", token, max_age: 1_209_600) do
|
|
||||||
{:ok, 0} ->
|
|
||||||
{:ok, socket}
|
|
||||||
|
|
||||||
{:ok, user_id} ->
|
|
||||||
user = Accounts.get_user(user_id)
|
|
||||||
{:ok, assign(socket, :user, user)}
|
|
||||||
|
|
||||||
{:error, _} = error ->
|
|
||||||
error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Socket id's are topics that allow you to identify all sockets for a given user:
|
|
||||||
#
|
|
||||||
# def id(socket), do: "user_socket:#{socket.assigns.user_id}"
|
|
||||||
#
|
|
||||||
# Would allow you to broadcast a "disconnect" event and terminate
|
|
||||||
# all active sockets and channels for a given user:
|
|
||||||
#
|
|
||||||
# Elixir.AlgoraWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
|
|
||||||
#
|
|
||||||
# Returning `nil` makes this socket anonymous.
|
|
||||||
@impl true
|
|
||||||
def id(_socket), do: nil
|
|
||||||
end
|
|
@ -810,7 +810,8 @@ defmodule AlgoraWeb.CoreComponents do
|
|||||||
"text-gray-50 focus:outline-none focus:ring-4 sm:text-sm sm:leading-6",
|
"text-gray-50 focus:outline-none focus:ring-4 sm:text-sm sm:leading-6",
|
||||||
"phx-no-feedback:border-gray-600 phx-no-feedback:focus:border-gray-500 phx-no-feedback:focus:ring-gray-100/5",
|
"phx-no-feedback:border-gray-600 phx-no-feedback:focus:border-gray-500 phx-no-feedback:focus:ring-gray-100/5",
|
||||||
"border-gray-600 focus:border-gray-500 focus:ring-gray-100/5",
|
"border-gray-600 focus:border-gray-500 focus:ring-gray-100/5",
|
||||||
@errors != [] && "border-red-500 focus:border-red-500 focus:ring-red-500/10"
|
@errors != [] &&
|
||||||
|
"border-red-500 focus:border-red-500 focus:ring-red-500/10 placeholder-red-300"
|
||||||
]}
|
]}
|
||||||
{@rest}
|
{@rest}
|
||||||
/>
|
/>
|
||||||
|
@ -57,9 +57,6 @@
|
|||||||
|
|
||||||
<link href="https://vjs.zencdn.net/8.10.0/video-js.css" rel="stylesheet" />
|
<link href="https://vjs.zencdn.net/8.10.0/video-js.css" rel="stylesheet" />
|
||||||
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
|
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
|
||||||
<script>
|
|
||||||
window.userToken = "<%= assigns[:user_token] %>";
|
|
||||||
</script>
|
|
||||||
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
|
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
|
||||||
</script>
|
</script>
|
||||||
<script defer data-domain="tv.algora.io" src="https://plausible.io/js/script.js">
|
<script defer data-domain="tv.algora.io" src="https://plausible.io/js/script.js">
|
||||||
|
@ -40,9 +40,6 @@
|
|||||||
|
|
||||||
<link href="https://vjs.zencdn.net/8.10.0/video-js.css" rel="stylesheet" />
|
<link href="https://vjs.zencdn.net/8.10.0/video-js.css" rel="stylesheet" />
|
||||||
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
|
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
|
||||||
<script>
|
|
||||||
window.userToken = "<%= assigns[:user_token] %>";
|
|
||||||
</script>
|
|
||||||
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
|
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}>
|
||||||
</script>
|
</script>
|
||||||
<script defer data-domain="tv.algora.io" src="https://plausible.io/js/script.js">
|
<script defer data-domain="tv.algora.io" src="https://plausible.io/js/script.js">
|
||||||
|
@ -91,11 +91,7 @@ defmodule AlgoraWeb.UserAuth do
|
|||||||
def fetch_current_user(conn, _opts) do
|
def fetch_current_user(conn, _opts) do
|
||||||
user_id = get_session(conn, :user_id)
|
user_id = get_session(conn, :user_id)
|
||||||
user = user_id && Accounts.get_user(user_id)
|
user = user_id && Accounts.get_user(user_id)
|
||||||
token = Phoenix.Token.sign(conn, "user socket", user_id || 0)
|
assign(conn, :current_user, user)
|
||||||
|
|
||||||
conn
|
|
||||||
|> assign(:current_user, user)
|
|
||||||
|> assign(:user_token, token)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
defmodule AlgoraWeb.Embed.Endpoint do
|
defmodule AlgoraWeb.Embed.Endpoint do
|
||||||
use Phoenix.Endpoint, otp_app: :algora
|
use Phoenix.Endpoint, otp_app: :algora
|
||||||
|
|
||||||
socket "/socket", AlgoraWeb.UserSocket,
|
|
||||||
websocket: true,
|
|
||||||
longpoll: false
|
|
||||||
|
|
||||||
socket "/live", Phoenix.LiveView.Socket
|
socket "/live", Phoenix.LiveView.Socket
|
||||||
|
|
||||||
# Serve at "/" the static files from "priv/static" directory.
|
# Serve at "/" the static files from "priv/static" directory.
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
defmodule AlgoraWeb.Endpoint do
|
defmodule AlgoraWeb.Endpoint do
|
||||||
use Phoenix.Endpoint, otp_app: :algora
|
use Phoenix.Endpoint, otp_app: :algora
|
||||||
|
|
||||||
socket "/socket", AlgoraWeb.UserSocket,
|
|
||||||
websocket: true,
|
|
||||||
longpoll: false
|
|
||||||
|
|
||||||
# The session will be stored in the cookie and signed,
|
# The session will be stored in the cookie and signed,
|
||||||
# this means its contents can be read but not tampered with.
|
# this means its contents can be read but not tampered with.
|
||||||
# Set :encryption_salt if you would also like to encrypt it.
|
# Set :encryption_salt if you would also like to encrypt it.
|
||||||
|
@ -20,10 +20,11 @@ defmodule AlgoraWeb.ChatLive do
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
id="chat-messages"
|
id="chat-messages"
|
||||||
phx-update="ignore"
|
phx-hook="Chat"
|
||||||
|
phx-update="stream"
|
||||||
class="text-sm break-words flex-1 m-1 scrollbar-thin overflow-y-auto inset-0 h-[400px] w-[400px] fixed overflow-hidden py-4 rounded ring-1 ring-purple-300"
|
class="text-sm break-words flex-1 m-1 scrollbar-thin overflow-y-auto inset-0 h-[400px] w-[400px] fixed overflow-hidden py-4 rounded ring-1 ring-purple-300"
|
||||||
>
|
>
|
||||||
<div :for={message <- @messages} id={"message-#{message.id}"} class="px-4">
|
<div :for={{id, message} <- @streams.messages} id={id} class="px-4">
|
||||||
<span class={"font-semibold #{if(system_message?(message), do: "text-emerald-400", else: "text-indigo-400")}"}>
|
<span class={"font-semibold #{if(system_message?(message), do: "text-emerald-400", else: "text-indigo-400")}"}>
|
||||||
<%= message.sender_handle %>:
|
<%= message.sender_handle %>:
|
||||||
</span>
|
</span>
|
||||||
@ -45,17 +46,18 @@ defmodule AlgoraWeb.ChatLive do
|
|||||||
Accounts.get_user_by!(handle: channel_handle)
|
Accounts.get_user_by!(handle: channel_handle)
|
||||||
|> Library.get_channel!()
|
|> Library.get_channel!()
|
||||||
|
|
||||||
|
video = Library.get_video!(video_id)
|
||||||
|
|
||||||
if connected?(socket) do
|
if connected?(socket) do
|
||||||
Library.subscribe_to_livestreams()
|
Library.subscribe_to_livestreams()
|
||||||
Library.subscribe_to_channel(channel)
|
Library.subscribe_to_channel(channel)
|
||||||
|
Chat.subscribe_to_room(video)
|
||||||
|
|
||||||
Presence.subscribe(channel_handle)
|
Presence.subscribe(channel_handle)
|
||||||
end
|
end
|
||||||
|
|
||||||
videos = Library.list_channel_videos(channel, 50)
|
videos = Library.list_channel_videos(channel, 50)
|
||||||
|
|
||||||
video = Library.get_video!(video_id)
|
|
||||||
|
|
||||||
subtitles = Library.list_subtitles(%Library.Video{id: video_id})
|
subtitles = Library.list_subtitles(%Library.Video{id: video_id})
|
||||||
|
|
||||||
data = %{}
|
data = %{}
|
||||||
@ -78,11 +80,11 @@ defmodule AlgoraWeb.ChatLive do
|
|||||||
channel: channel,
|
channel: channel,
|
||||||
videos_count: Enum.count(videos),
|
videos_count: Enum.count(videos),
|
||||||
video: video,
|
video: video,
|
||||||
subtitles: subtitles,
|
subtitles: subtitles
|
||||||
messages: Chat.list_messages(video)
|
|
||||||
)
|
)
|
||||||
|> assign_form(changeset)
|
|> assign_form(changeset)
|
||||||
|> stream(:videos, videos)
|
|> stream(:videos, videos)
|
||||||
|
|> stream(:messages, Chat.list_messages(video))
|
||||||
|> stream(:presences, Presence.list_online_users(channel_handle))
|
|> stream(:presences, Presence.list_online_users(channel_handle))
|
||||||
|
|
||||||
if connected?(socket), do: send(self(), {:play, video})
|
if connected?(socket), do: send(self(), {:play, video})
|
||||||
@ -158,10 +160,14 @@ defmodule AlgoraWeb.ChatLive do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def handle_info(
|
def handle_info(
|
||||||
{Library, %Library.Events.MessageDeleted{message: message}},
|
{Chat, %Chat.Events.MessageDeleted{message: message}},
|
||||||
socket
|
socket
|
||||||
) do
|
) do
|
||||||
{:noreply, socket |> push_event("message_deleted", %{id: message.id})}
|
{:noreply, socket |> stream_delete(:messages, message)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({Chat, %Chat.Events.MessageSent{message: message}}, socket) do
|
||||||
|
{:noreply, socket |> stream_insert(:messages, message)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info({Library, _}, socket), do: {:noreply, socket}
|
def handle_info({Library, _}, socket), do: {:noreply, socket}
|
||||||
|
@ -6,6 +6,7 @@ defmodule AlgoraWeb.VideoLive do
|
|||||||
alias AlgoraWeb.{LayoutComponent, Presence}
|
alias AlgoraWeb.{LayoutComponent, Presence}
|
||||||
alias AlgoraWeb.ChannelLive.{StreamFormComponent}
|
alias AlgoraWeb.ChannelLive.{StreamFormComponent}
|
||||||
|
|
||||||
|
@impl true
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="lg:mr-[24rem]">
|
<div class="lg:mr-[24rem]">
|
||||||
@ -215,7 +216,7 @@ defmodule AlgoraWeb.VideoLive do
|
|||||||
<div class={[
|
<div class={[
|
||||||
"overflow-y-auto text-sm break-words flex-1 scrollbar-thin",
|
"overflow-y-auto text-sm break-words flex-1 scrollbar-thin",
|
||||||
if(@can_edit,
|
if(@can_edit,
|
||||||
do: "h-[calc(100vh-11rem)]",
|
do: "h-[calc(100vh-12rem)]",
|
||||||
else: "h-[calc(100vh-8.75rem)]"
|
else: "h-[calc(100vh-8.75rem)]"
|
||||||
)
|
)
|
||||||
]}>
|
]}>
|
||||||
@ -241,13 +242,13 @@ defmodule AlgoraWeb.VideoLive do
|
|||||||
<.simple_form
|
<.simple_form
|
||||||
:if={@can_edit}
|
:if={@can_edit}
|
||||||
id="edit-transcript"
|
id="edit-transcript"
|
||||||
for={@form}
|
for={@transcript_form}
|
||||||
phx-submit="save"
|
phx-submit="save"
|
||||||
phx-update="ignore"
|
phx-update="ignore"
|
||||||
class="hidden h-full px-4"
|
class="hidden h-full px-4"
|
||||||
>
|
>
|
||||||
<.input
|
<.input
|
||||||
field={@form[:subtitles]}
|
field={@transcript_form[:subtitles]}
|
||||||
type="textarea"
|
type="textarea"
|
||||||
label="Edit transcript"
|
label="Edit transcript"
|
||||||
class="font-mono h-[calc(100vh-14.75rem)]"
|
class="font-mono h-[calc(100vh-14.75rem)]"
|
||||||
@ -274,12 +275,13 @@ defmodule AlgoraWeb.VideoLive do
|
|||||||
<div :if={tab == :chat}>
|
<div :if={tab == :chat}>
|
||||||
<div
|
<div
|
||||||
id="chat-messages"
|
id="chat-messages"
|
||||||
phx-update="ignore"
|
phx-hook="Chat"
|
||||||
class="text-sm break-words flex-1 scrollbar-thin overflow-y-auto h-[calc(100vh-11rem)]"
|
phx-update="stream"
|
||||||
|
class="text-sm break-words flex-1 scrollbar-thin overflow-y-auto h-[calc(100vh-12rem)]"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
:for={message <- @messages}
|
:for={{id, message} <- @streams.messages}
|
||||||
id={"message-#{message.id}"}
|
id={id}
|
||||||
class="group hover:bg-white/5 relative px-4"
|
class="group hover:bg-white/5 relative px-4"
|
||||||
>
|
>
|
||||||
<span class={"font-semibold #{if(system_message?(message), do: "text-emerald-400", else: "text-indigo-400")}"}>
|
<span class={"font-semibold #{if(system_message?(message), do: "text-emerald-400", else: "text-indigo-400")}"}>
|
||||||
@ -301,13 +303,14 @@ defmodule AlgoraWeb.VideoLive do
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-4">
|
<div class="px-4">
|
||||||
<input
|
<.simple_form
|
||||||
:if={@current_user}
|
:if={@current_user}
|
||||||
id="chat-input"
|
for={@chat_form}
|
||||||
placeholder="Send a message"
|
phx-submit="send"
|
||||||
disabled={@current_user == nil}
|
phx-change="validate"
|
||||||
class="mt-2 bg-gray-950 h-[30px] text-white focus:outline-none focus:ring-purple-400 block w-full min-w-0 rounded-md sm:text-sm ring-1 ring-gray-600 px-2"
|
>
|
||||||
/>
|
<.input field={@chat_form[:body]} placeholder="Send a message" autocomplete="off" />
|
||||||
|
</.simple_form>
|
||||||
<a
|
<a
|
||||||
:if={!@current_user}
|
:if={!@current_user}
|
||||||
href={Algora.Github.authorize_url()}
|
href={Algora.Github.authorize_url()}
|
||||||
@ -330,6 +333,7 @@ defmodule AlgoraWeb.VideoLive do
|
|||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
def mount(
|
def mount(
|
||||||
%{"channel_handle" => channel_handle, "video_id" => video_id} = params,
|
%{"channel_handle" => channel_handle, "video_id" => video_id} = params,
|
||||||
_session,
|
_session,
|
||||||
@ -341,9 +345,12 @@ defmodule AlgoraWeb.VideoLive do
|
|||||||
Accounts.get_user_by!(handle: channel_handle)
|
Accounts.get_user_by!(handle: channel_handle)
|
||||||
|> Library.get_channel!()
|
|> Library.get_channel!()
|
||||||
|
|
||||||
|
video = Library.get_video!(video_id)
|
||||||
|
|
||||||
if connected?(socket) do
|
if connected?(socket) do
|
||||||
Library.subscribe_to_livestreams()
|
Library.subscribe_to_livestreams()
|
||||||
Library.subscribe_to_channel(channel)
|
Library.subscribe_to_channel(channel)
|
||||||
|
Chat.subscribe_to_room(video)
|
||||||
|
|
||||||
Presence.track_user(channel_handle, %{
|
Presence.track_user(channel_handle, %{
|
||||||
id: if(current_user, do: current_user.handle, else: "")
|
id: if(current_user, do: current_user.handle, else: "")
|
||||||
@ -354,8 +361,6 @@ defmodule AlgoraWeb.VideoLive do
|
|||||||
|
|
||||||
videos = Library.list_channel_videos(channel, 50)
|
videos = Library.list_channel_videos(channel, 50)
|
||||||
|
|
||||||
video = Library.get_video!(video_id)
|
|
||||||
|
|
||||||
subtitles = Library.list_subtitles(%Library.Video{id: video_id})
|
subtitles = Library.list_subtitles(%Library.Video{id: video_id})
|
||||||
|
|
||||||
data = %{}
|
data = %{}
|
||||||
@ -367,7 +372,7 @@ defmodule AlgoraWeb.VideoLive do
|
|||||||
|
|
||||||
types = %{subtitles: :string}
|
types = %{subtitles: :string}
|
||||||
|
|
||||||
changeset =
|
transcript_changeset =
|
||||||
{data, types}
|
{data, types}
|
||||||
|> Ecto.Changeset.cast(%{subtitles: encoded_subtitles}, Map.keys(types))
|
|> Ecto.Changeset.cast(%{subtitles: encoded_subtitles}, Map.keys(types))
|
||||||
|
|
||||||
@ -381,14 +386,15 @@ defmodule AlgoraWeb.VideoLive do
|
|||||||
videos_count: Enum.count(videos),
|
videos_count: Enum.count(videos),
|
||||||
video: video,
|
video: video,
|
||||||
subtitles: subtitles,
|
subtitles: subtitles,
|
||||||
messages: Chat.list_messages(video),
|
|
||||||
tabs: tabs,
|
tabs: tabs,
|
||||||
# TODO: reenable once fully implemented
|
# TODO: reenable once fully implemented
|
||||||
# associated segments need to be removed from db & vectorstore
|
# associated segments need to be removed from db & vectorstore
|
||||||
can_edit: false
|
can_edit: false,
|
||||||
|
transcript_form: to_form(transcript_changeset, as: :data),
|
||||||
|
chat_form: to_form(Chat.change_message(%Chat.Message{}))
|
||||||
)
|
)
|
||||||
|> assign_form(changeset)
|
|
||||||
|> stream(:videos, videos)
|
|> stream(:videos, videos)
|
||||||
|
|> stream(:messages, Chat.list_messages(video))
|
||||||
|> stream(:presences, Presence.list_online_users(channel_handle))
|
|> stream(:presences, Presence.list_online_users(channel_handle))
|
||||||
|
|
||||||
if connected?(socket), do: send(self(), {:play, {video, params["t"]}})
|
if connected?(socket), do: send(self(), {:play, {video, params["t"]}})
|
||||||
@ -396,11 +402,13 @@ defmodule AlgoraWeb.VideoLive do
|
|||||||
{:ok, socket}
|
{:ok, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
def handle_params(params, _url, socket) do
|
def handle_params(params, _url, socket) do
|
||||||
LayoutComponent.hide_modal()
|
LayoutComponent.hide_modal()
|
||||||
{:noreply, socket |> apply_action(socket.assigns.live_action, params)}
|
{:noreply, socket |> apply_action(socket.assigns.live_action, params)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
def handle_info({:play, {video, t}}, socket) do
|
def handle_info({:play, {video, t}}, socket) do
|
||||||
socket =
|
socket =
|
||||||
socket
|
socket
|
||||||
@ -476,10 +484,14 @@ defmodule AlgoraWeb.VideoLive do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def handle_info(
|
def handle_info(
|
||||||
{Library, %Library.Events.MessageDeleted{message: message}},
|
{Chat, %Chat.Events.MessageDeleted{message: message}},
|
||||||
socket
|
socket
|
||||||
) do
|
) do
|
||||||
{:noreply, socket |> push_event("message_deleted", %{id: message.id})}
|
{:noreply, socket |> stream_delete(:messages, message)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({Chat, %Chat.Events.MessageSent{message: message}}, socket) do
|
||||||
|
{:noreply, socket |> stream_insert(:messages, message)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info({Library, _}, socket), do: {:noreply, socket}
|
def handle_info({Library, _}, socket), do: {:noreply, socket}
|
||||||
@ -496,6 +508,36 @@ defmodule AlgoraWeb.VideoLive do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("validate", %{"message" => %{"body" => ""}}, socket), do: {:noreply, socket}
|
||||||
|
|
||||||
|
def handle_event("validate", %{"message" => params}, socket) do
|
||||||
|
form =
|
||||||
|
%Chat.Message{}
|
||||||
|
|> Chat.change_message(params)
|
||||||
|
|> Map.put(:action, :insert)
|
||||||
|
|> to_form()
|
||||||
|
|
||||||
|
{:noreply, assign(socket, chat_form: form)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("send", %{"message" => %{"body" => ""}}, socket), do: {:noreply, socket}
|
||||||
|
|
||||||
|
def handle_event("send", %{"message" => params}, socket) do
|
||||||
|
%{current_user: current_user, video: video} = socket.assigns
|
||||||
|
|
||||||
|
case Chat.create_message(current_user, video, params) do
|
||||||
|
{:ok, message} ->
|
||||||
|
# HACK:
|
||||||
|
message = Chat.get_message!(message.id)
|
||||||
|
Chat.broadcast_message_sent!(message)
|
||||||
|
{:noreply, assign(socket, chat_form: to_form(Chat.change_message(%Chat.Message{})))}
|
||||||
|
|
||||||
|
{:error, %Ecto.Changeset{} = changeset} ->
|
||||||
|
{:noreply, assign(socket, chat_form: to_form(changeset))}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def handle_event("save", %{"data" => %{"subtitles" => subtitles}, "save" => save_type}, socket) do
|
def handle_event("save", %{"data" => %{"subtitles" => subtitles}, "save" => save_type}, socket) do
|
||||||
{time, count} = :timer.tc(&save/2, [save_type, subtitles])
|
{time, count} = :timer.tc(&save/2, [save_type, subtitles])
|
||||||
msg = "Updated #{count} subtitles in #{fmt(round(time / 1000))} ms"
|
msg = "Updated #{count} subtitles in #{fmt(round(time / 1000))} ms"
|
||||||
@ -509,7 +551,7 @@ defmodule AlgoraWeb.VideoLive do
|
|||||||
|
|
||||||
if current_user && Chat.can_delete?(current_user, message) do
|
if current_user && Chat.can_delete?(current_user, message) do
|
||||||
{:ok, message} = Chat.delete_message(message)
|
{:ok, message} = Chat.delete_message(message)
|
||||||
Library.broadcast_message_deleted!(socket.assigns.channel, message)
|
Chat.broadcast_message_deleted!(message)
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
else
|
else
|
||||||
{:noreply, socket |> put_flash(:error, "You can't do that")}
|
{:noreply, socket |> put_flash(:error, "You can't do that")}
|
||||||
@ -546,10 +588,6 @@ defmodule AlgoraWeb.VideoLive do
|
|||||||
if cond, do: list ++ [extra], else: list
|
if cond, do: list ++ [extra], else: list
|
||||||
end
|
end
|
||||||
|
|
||||||
defp assign_form(socket, %Ecto.Changeset{} = changeset) do
|
|
||||||
assign(socket, :form, to_form(changeset, as: :data))
|
|
||||||
end
|
|
||||||
|
|
||||||
defp apply_action(socket, :stream, _params) do
|
defp apply_action(socket, :stream, _params) do
|
||||||
if socket.assigns.owns_channel? do
|
if socket.assigns.owns_channel? do
|
||||||
socket
|
socket
|
||||||
|
Loading…
x
Reference in New Issue
Block a user