Merge pull request #4 from JasterV/feat/share-session

feat: share session
This commit is contained in:
Víctor Martínez 2024-03-30 02:05:43 +01:00 committed by GitHub
commit 68834cbcb2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 196 additions and 104 deletions

View file

@ -0,0 +1,20 @@
const CopyToClipboard = () => {
return {
mounted() {
const initialInnerHTML = this.el.innerHTML;
const { textToCopy } = this.el.dataset;
this.el.addEventListener("click", () => {
navigator.clipboard.writeText(textToCopy);
this.el.innerHTML = "Copied!";
setTimeout(() => {
this.el.innerHTML = initialInnerHTML;
}, 2000);
});
},
};
};
export default CopyToClipboard;

View file

@ -2,9 +2,13 @@ import { socketConfig } from "./setup";
import { Socket } from "phoenix";
import { LiveSocket } from "phoenix_live_view";
import HubHook from "./hubHook";
import NativeShareHook from "./nativeShareHook";
import CopyToClipboardHook from "./copyToClipboardHook";
const Hooks = {
HubHook: HubHook(),
Hub: HubHook(),
NativeShare: NativeShareHook(),
CopyToClipboard: CopyToClipboardHook(),
};
const liveSocket = new LiveSocket("/live", Socket, {

View file

@ -0,0 +1,27 @@
const NativeShare = () => {
return {
mounted() {
this.el.addEventListener("click", async () => {
// Check if sharing is supported
if (!navigator.canShare) {
this.pushEvent("url_share_error", {
error: "Unfortunately, sharing is not supported",
});
return;
}
try {
await navigator.share({
title: this.el.dataset.title,
text: this.el.dataset.text,
url: this.el.dataset.url,
});
this.pushEvent("url_shared", {});
} catch (error) {
this.pushEvent("url_share_error", { error: error.message });
}
});
},
};
};
export default NativeShare;

View file

@ -18,17 +18,17 @@ defmodule IntisyncWeb.LiveViewMonitor do
end
def handle_call({:monitor, pid, view_module, meta}, _, %{views: views} = state) do
mref = Process.monitor(pid)
{:reply, :ok, %{state | views: Map.put(views, pid, {view_module, meta, mref})}}
_ref = Process.monitor(pid)
{:reply, :ok, %{state | views: Map.put(views, pid, {view_module, meta})}}
end
def handle_info({:DOWN, _ref, :process, pid, reason}, state) do
{{module, meta, mref}, new_views} = Map.pop(state.views, pid)
def handle_info({:DOWN, ref, :process, pid, reason}, state) do
{{module, meta}, new_views} = Map.pop(state.views, pid)
Process.demonitor(ref)
Task.start(fn -> module.unmount(reason, meta) end)
Process.demonitor(mref)
{:noreply, %{state | views: new_views}}
end
end

View file

@ -18,68 +18,6 @@ defmodule IntisyncWeb.CoreComponents do
alias Phoenix.LiveView.JS
@doc """
Renders a modal.
## Examples
<.modal id="confirm-modal">
This is a modal.
</.modal>
JS commands may be passed to the `:on_cancel` to configure
the closing/cancel event, for example:
<.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}>
This is another modal.
</.modal>
"""
attr :id, :string, required: true
slot :inner_block, required: true
def modal(assigns) do
~H"""
<div id={@id} phx-remove={"#{@id}-close"} class="relative z-50">
<div id={"#{@id}-bg"} class="bg-zinc-50/90 fixed inset-0" aria-hidden="true" />
<div
class="fixed inset-0 overflow-y-auto"
aria-labelledby={"#{@id}-title"}
aria-describedby={"#{@id}-description"}
role="dialog"
aria-modal="true"
tabindex="0"
>
<div class="flex min-h-full items-center justify-center">
<div class="w-full max-w-3xl p-4 sm:p-6 lg:py-8">
<.focus_wrap
id={"#{@id}-container"}
phx-window-keydown={"#{@id}-close"}
phx-key="escape"
phx-click-away={"#{@id}-close"}
class="shadow-zinc-700/10 ring-zinc-700/10 relative rounded-2xl bg-white p-14 shadow-lg ring-1"
>
<div class="absolute top-6 right-5">
<button
phx-click={"#{@id}-close"}
type="button"
class="-m-3 flex-none p-3 opacity-20 hover:opacity-40"
aria-label="close"
>
<.icon name="hero-x-mark-solid" class="h-5 w-5" />
</button>
</div>
<div id={"#{@id}-content"}>
<%= render_slot(@inner_block) %>
</div>
</.focus_wrap>
</div>
</div>
</div>
</div>
"""
end
@doc """
Renders flash notices.

View file

@ -12,7 +12,7 @@
</div>
</div>
</header>
<main class="px-4 py-20 sm:px-6 lg:px-8">
<main class="px-4 py-10 sm:px-6 lg:px-8">
<div class="mx-auto max-w-2xl">
<.flash_group flash={@flash} />
<%= @inner_content %>

View file

@ -15,6 +15,7 @@ defmodule IntisyncWeb.HubLive do
|> assign(:intiface_client_status, nil)
|> assign(:devices, %{})
|> assign(:session_id, nil)
|> assign(:share_url, nil)
{:ok, socket}
end
@ -31,6 +32,7 @@ defmodule IntisyncWeb.HubLive do
|> assign(:remote_controller_status, :disconnected)
|> assign(:intiface_client_status, :disconnected)
|> assign(:session_id, session_id)
|> assign(:share_url, share_url(session_id))
if exists_session?(session_id) do
socket = socket |> put_flash(:error, "Unauthorized") |> redirect(to: "/")
@ -63,6 +65,8 @@ defmodule IntisyncWeb.HubLive do
SessionPubSub.subscribe!(session_id, "devices", "vibrate")
end
defp share_url(session_id), do: IntisyncWeb.Endpoint.url() <> ~p"/sessions/#{session_id}/remote"
############################
# Remote controller events #
############################
@ -92,6 +96,18 @@ defmodule IntisyncWeb.HubLive do
{:noreply, socket}
end
#############
# UI events #
#############
def handle_event("url_shared", %{}, socket) do
{:noreply, put_flash(socket, :info, "Session shared! :)")}
end
def handle_event("url_share_error", %{"error" => error}, socket) do
{:noreply, put_flash(socket, :error, "Failed to share session. #{error}")}
end
##########################
# Intiface Client events #
##########################

View file

@ -6,7 +6,7 @@
You are hosting an IntiSync session
</p>
<div id="hub-status" class="flex flex-col sm:flex-row items-left gap-3 mt-6">
<div id="hub-status" class="flex flex-row gap-3 mt-6">
<.badge
:if={@intiface_client_status == :disconnected}
id="intiface-client-disconnected-badge"
@ -41,34 +41,90 @@
</div>
</header>
<section
:if={@intiface_client_status == :disconnected}
id="connect-buttons"
class="flex flex-col gap-6 items-center"
>
<.button id="intiface-connect-button" type="button" phx-click="connect">
Connect to Intiface Central
</.button>
</section>
<main class="flex flex-col gap-5 mt-10">
<section
:if={@remote_controller_status == :disconnected}
id="share-session"
class="text-center rounded-lg border-solid border-zinc-200 border-2 p-6"
>
<h3 class="text-2xl mb-6 font-semibold text-zinc-600">
Share this session to start having fun
</h3>
<section :if={@intiface_client_status == :connected} id="connected-devices-section">
<h2 class="text-3xl mb-4 font-bold text-indigo-500">
Connected devices
</h2>
<div class="flex flex-col items-center gap-4">
<div class="flex w-full items-center">
<span class="rounded-s-lg z-10 inline-flex flex-shrink-0 items-center border border-gray-300 bg-indigo-500 px-4 py-2.5 text-center text-sm font-medium text-white">
URL
</span>
<div class="w-full">
<input
id="share-url"
type="text"
aria-describedby="helper-text-explanation"
class="border-e-0 border-s-0 block w-full border border-gray-300 p-2.5 text-sm text-gray-500"
value={@share_url}
readonly
disabled
/>
</div>
<.button
id="copyButton"
phx-hook="CopyToClipboard"
data-text-to-copy={@share_url}
class="rounded-r-lg rounded-l-none flex-shrink-0 text-sm px-3 py-2 text-center "
>
<.icon name="hero-document-duplicate-solid" class="mr-1.5 -ml-0.5" /> Copy
</.button>
</div>
<p :if={Enum.empty?(@devices)} class="text-lg text-amber-500 font-semibold">
<.icon name="hero-signal-slash" class="mr-1.5" /> No devices connected yet
</p>
<p class="font-semibold text-zinc-600">Or</p>
<div class="flex flex-col gap-5 mt-2">
<%= for {index, device} <- @devices do %>
<IntisyncWeb.DeviceCardComponent.view
id={"device-#{index}"}
device={device}
disabled={true}
/>
<% end %>
</div>
</section>
<.button
id="shareButton"
phx-hook="NativeShare"
data-url={@share_url}
data-title="Share session url"
data-text="Join an IntiSync session"
phx-update="ignore"
>
<.icon name="hero-share-solid" class="mr-1.5 -ml-0.5" /> Share
</.button>
</div>
</section>
<div id={@session_id} phx-hook="HubHook"></div>
<section
:if={@intiface_client_status == :disconnected}
id="intiface-central-connect"
class="text-center rounded-lg border-solid border-zinc-200 border-2 p-6"
>
<h3 class="text-2xl mb-6 font-semibold text-zinc-600">
Connect to Intiface Central to control your devices
</h3>
<.button id="intiface-connect-button" type="button" phx-click="connect">
<.icon name="hero-link-solid" class="mr-1.5 -ml-0.5" /> Connect
</.button>
</section>
<section :if={@intiface_client_status == :connected} id="connected-devices-section">
<h2 class="text-3xl mb-4 font-bold text-indigo-500">
Connected devices
</h2>
<p :if={Enum.empty?(@devices)} class="text-lg text-amber-500 font-semibold">
<.icon name="hero-signal-slash" class="mr-1.5" /> No devices connected yet
</p>
<div class="flex flex-col gap-5 mt-2">
<%= for {index, device} <- @devices do %>
<IntisyncWeb.DeviceCardComponent.view
id={"device-#{index}"}
device={device}
disabled={true}
/>
<% end %>
</div>
</section>
</main>
<div id={@session_id} phx-hook="Hub"></div>

View file

@ -108,16 +108,47 @@ defmodule IntisyncWeb.HubLiveTest do
refute view |> element("#remote-controller-connected-badge") |> has_element?()
end
test "Share session section is shown when controller is disconnected", %{
conn: conn,
session_id: session_id
} do
{:ok, view, _html} = live(conn, ~p"/sessions/#{session_id}")
assert view |> element("#share-session") |> has_element?()
end
test "Share session section is not shown when controller connects", %{
conn: conn,
session_id: session_id
} do
{:ok, view, _html} = live(conn, ~p"/sessions/#{session_id}")
{:ok, remote_view, _html} = live(conn, ~p"/sessions/#{session_id}/remote")
refute view |> element("#share-session") |> has_element?()
end
test "Share session section is shown again when controller disconnects", %{
conn: conn,
session_id: session_id
} do
{:ok, view, _html} = live(conn, ~p"/sessions/#{session_id}")
{:ok, remote_view, _html} = live(conn, ~p"/sessions/#{session_id}/remote")
assert remote_view |> element("#nav-home-btn") |> render_click() |> follow_redirect(conn)
assert view |> element("#share-session") |> has_element?()
end
test "The connect buttons section gets replaced by the devices section when intiface client connects",
%{conn: conn, session_id: session_id} do
{:ok, view, _html} = live(conn, ~p"/sessions/#{session_id}")
assert view |> element("#connect-buttons") |> has_element?()
assert view |> element("#intiface-connect-button") |> has_element?()
refute view |> element("#connected-devices-section") |> has_element?()
render_click(view, "connected", %{})
refute view |> element("#connect-buttons") |> has_element?()
refute view |> element("#intiface-connect-button") |> has_element?()
assert view |> element("#connected-devices-section") |> has_element?()
end
@ -127,12 +158,12 @@ defmodule IntisyncWeb.HubLiveTest do
render_click(view, "connected", %{})
refute view |> element("#connect-buttons") |> has_element?()
refute view |> element("#intiface-connect-button") |> has_element?()
assert view |> element("#connected-devices-section") |> has_element?()
render_click(view, "disconnected", %{})
assert view |> element("#connect-buttons") |> has_element?()
assert view |> element("#intiface-connect-button") |> has_element?()
refute view |> element("#connected-devices-section") |> has_element?()
end
@ -140,7 +171,7 @@ defmodule IntisyncWeb.HubLiveTest do
%{conn: conn, session_id: session_id} do
{:ok, view, _html} = live(conn, ~p"/sessions/#{session_id}")
assert view |> element("#connect-buttons") |> has_element?()
assert view |> element("#intiface-connect-button") |> has_element?()
assert view |> element("#intiface-connect-button") |> render_click()
@ -229,7 +260,7 @@ defmodule IntisyncWeb.HubLiveTest do
render_click(view, "disconnected", %{})
assert view |> element("#connect-buttons") |> has_element?()
assert view |> element("#intiface-connect-button") |> has_element?()
refute view |> element("#connected-devices-section") |> has_element?()
render_click(view, "connected", %{})