Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Prod Trigger] Migration for Thiruvasagam #98

Closed
wants to merge 328 commits into from
Closed

Conversation

ks0m1c
Copy link
Contributor

@ks0m1c ks0m1c commented Aug 31, 2024

No description provided.

rtshkmr and others added 30 commits February 9, 2024 00:11
The elapsed timing is likely wrong though, that's why the resume play
doesn't work well.
* Foundations of the CLI

* Fixing Finch Behaviour for Escripts

* Wayback Fallback

* File Storage and Chalisa.json done

* Support fork for escripts vs application runtime

---------

Co-authored-by: ks0m1c_dharma <sakiyamuni@sams.ara>
Co-authored-by: Ritesh Kumar <ritesh@emerald.pink>
Also keeps voice within playback in the player_live's assigns state.

Still yet to shift other playback content responsibilities to media lib.
There are a few bugs:
1. [minor] the current time display is not updating properly
2. [unsure] the emphasis is not happening at exact times, unsure if it's
data issue (i.e. origin values are incorrect) or state-handling issue
3. [CSS - unsure] the sticky player is sticking to its own container
div, hence it won't stick atop the container that contains all the
page's contents
4. [unsure] if you use a system control to control the HTML5 player's
playback, then the playback is correctly controlled but the application
doesn't listen to this event, so state updates don't sync up
* shifted playback functions from medium to playback.ex

* added guard in audio player for no-events case

* reintroduced req as a dep because the corpus/engine/fallback.ex uses
that lib
- extracted out to components within player_live
- hid the next and prev buttons on the player
- added other minor player "fixes" like the current playing time and
- such
Media Player + Ramayanam Scraper
@rtshkmr rtshkmr changed the title Migration for Thiruvasagam [Prod Trigger] Migration for Thiruvasagam Sep 1, 2024
rtshkmr and others added 21 commits September 4, 2024 21:38
This is a big commit, commit messages to signpost will be added to this.

Essentially, we want DM to be a mediator and therefore all the
mode-specific logic should be taken out of the DM and into the
mode-specific content module.

ReadingContent is the component that shall be slotted in for the "read"
mode.

This particular commit is not a complete refactor, but takes the
following steps towards that direction:
1. shifts the read-mode-specific data loading to the content module.
this means all the apply actions have been updated.

2. adhere to the pattern where content livecomponent is it's own source
of truth (this part is not fully refactored yet, so DM still injects
some state into the content livecomponent). This particular pattern
seems to actually follow [known Phoenix patterns actually](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveComponent.html#module-livecomponent-as-the-source-of-truth).
    * the liveview only prop-passes the id
    * comms b/w liveview and livecomponent happen using =send_update()=, this is why the =id= for the livecomponent is imporant

3. update event emitters within HEEX strings such that all the events
are targetted. For example, the "clickVerseToSeek" will target dom id =
`#reading-content`. This adheres to the pattern where mode-specific
functionalities happen within their respective components. In this
example, VerseMatrix is a nested child of verse, which is a child of the
reading_content live component. Since this particular event has nothign
to do with other generic slots (e.g. actionBar), then there's no need
for DM to be involved with the handling of this event.

4. DM shall keep things generic, Content is a new slot, and arguments
for it shall be provided by the

5. NOTE: the MediaBridge will be reworked in the next few PRs, which
would simplify how the handshakes happen. For now, this commit actually
introduces some regression bug in the handshake actually. We could spend
a short 30min trying to debug it, but it should be alright to ignore
this regression bug until we get to the point where MediaBridge is
getting refactored, during which we can fix it altogether.

Next Steps:
1. complete point 1 above, there's still the hoverrune and other
read-mode specific events that are yet to be ported.
******* HoverRune hook expects parent id
  * HoverRune is a generic hook and should work in a manner specific to the mode we are currently in
  * HoverRune shall expect a parent id to be provided
    + this gets supplied by the heex template like so
      #+begin_src html
        <div id="verses" phx-update="stream" phx-hook="HoveRune" data-parent-id="reading-content">
      #+end_src
    + this allows us to target the event pushed from client to the server
      #+begin_src js
          this.pushEventTo(`#${this.parentId}`, "bindHoveRune", {
            binding: binding,
          });
      #+end_src
    + so if in reading mode, then parentId = "reading-content" and so VyasaWeb.Content.ReadingContent can handle the bindHoverRune event.

<a id="orgfcd7294"></a>

-   HoverRune is a generic hook and should work in a manner specific to the mode we are currently in
-   HoverRune shall expect a parent id to be provided
    -   this gets supplied by the heex template like so
        ```html
        <div id="verses" phx-update="stream" phx-hook="HoveRune" data-parent-id="reading-content">
        ```
    -   this allows us to target the event pushed from client to the server

        ```js
            this.pushEventTo(`#${this.parentId}`, "bindHoveRune", {
              binding: binding,
            });
        ```

    -   so if in reading mode, then parentId = &ldquo;reading-content&rdquo; and so VyasaWeb.Content.ReadingContent can handle the bindHoverRune event.
This commit offers a good example of what the communication patterns b/w
parent liveview and child livecomponent can happen when we choose to
keep child livecomponents as the source of truth for data specific to a
user_mode.

<a id="orgc6f88a0"></a>

Here are some points to consider:

-   since the parent live view is a live view, it can subscribe to topics on the pubsub system and do the necessary handle<sub>info</sub>(). If the effect is mode-agnostic then the parent handles it, else it shall delegate to the child. If the child needs to communicate to the parent, then it shall just message-pass to the parent via a `send()`.

    This actually follows a documented pattern in LiveView in which (nested) LiveComponents are the source of (data) truth [[ref](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveComponent.html#module-livecomponent-as-the-source-of-truth)].

    let&rsquo;s use an example to describe how this communication b/w parent and child is doneand:

    1.  parent receives message for :media<sub>handshake</sub>::init
    2.  parent calls `send_update(ReadingContent, id: "reading-content", sess_id: sess_id)`, which allows the parent to target the specific child that it needs to delegate to. As we can observe, this example is hardcoded (i.e. ReadingContent is NOT generic). In reality, we expect the parent to refer to names generically. For example:
        `send_update(@mode.Content.Component, id: @mode.Content.id, sess_id: sess_id)`
    3.  on the child&rsquo;s side, the update() or update<sub>many</sub>() function can be pattern-matched for this specific delegation-action. For example:

      ```elixir
            @impl true
            # received updates from parent liveview when a handshake is init, does a pub for the voice to use
            def update(
                  %{id: "reading-content", sess_id: sess_id} = _props,
                  %{
                    assigns: %{
                      session: %{"id" => sess_id},
                      chap: %Chapter{no: c_no, source_id: src_id}
                    }
                  } = socket
                ) do
              case Medium.get_voice(src_id, c_no, @default_voice_lang) do
                %Voice{} = v ->
                  Vyasa.PubSub.publish(
                    v,
                    :voice_ack,
                    sess_id
                  )

                _ ->
                  nil
              end

              {:ok, socket}
            end

      ```

    4.  Separately, if the child wishes to communicate to the parent, then it shall message-pass like so:

      ```elixir
            @impl true
            def handle_event(
                  "verses::focus_toggle_on_quick_mark_drafting",
                  %{"is_focusing?" => is_focusing?} = _payload,
                  %Socket{} = socket
                ) do
              send(self(), {:change_ui, "update_media_bridge_visibility", [is_focusing?]})

              {:noreply, socket}
            end
      ```

-   The message passing from the child to the parent may involve triggering some other functions to trigger a separate pipeline of effects.
    Here, we may want to use a dynamic handle<sub>info</sub>() on the parent&rsquo;s side that would allow us the flexibility to apply whatever action(function) we want.
    This would allow the child to trigger functions within the parent.

    For example,

    1.  say the child component wants to update the `%UiState{}`, which is the held by the parent.

      ```elixir
            # within the child:
            @impl true
            def handle_event(
                  "verses::focus_toggle_on_quick_mark_drafting",
                  %{"is_focusing?" => is_focusing?} = _payload,
                  %Socket{} = socket
                ) do
              send(self(), {:change_ui, "update_media_bridge_visibility", [is_focusing?]})

              {:noreply, socket}
            end
       ```

    2.  then on the parent side, we could handle it like so:

      ```elixir
            # parent liveview:
              @impl true
              def handle_info({:change_ui, function_name, args}, socket)
                  when is_binary(function_name) and is_list(args) do
                with :ok <- validate_function_name(function_name, UiState) do
                  func = String.to_existing_atom(function_name)
                  updated_socket = apply(UiState, func, [socket | args])
                  {:noreply, updated_socket}
                else
                  {:error, reason} ->
                    IO.puts("Error: #{reason}")
                    {:noreply, socket}
                end
              end
       ```
Nothing special here, same patterns as before.
The selectors are hydrated dynamically
The `%UserMode{}` struct expects slots to be defined within it. This
will be either nil or components (defined by their Module name e.g.
`control_panel_component: VyasaWeb.ControlPanel`).

The slots expected are defined within `@component_slots` e.g.

``` elixir
@component_slots [:action_bar_component, :control_panel_component, :mode_context_component]
```

The struct is dynamically defined, like so:

``` elixir
@derive Jason.Encoder
defstruct [
            :mode,
            :default_ui_state,
            :mode_icon_name,
            :quick_actions,
            :control_panel_modes,
            :mode_actions,
            :action_bar_actions,
            :action_bar_info_types
          ] ++
            Enum.flat_map(@component_slots, fn slot ->
              [{slot, nil}, {:"#{slot}_selector", @default_slot_selector}]
            end)
```

So every `<SLOT>` will have an associated `<SLOT>_selector`

For this to work, the selectors have to be hydrated. A little annoying
but `%UserMode{}` should not be defined by itself, it should only be
accessed via the exposed `get_mode()` functions, which automatically
handle the hydration of selectors:

``` elixir
def get_initial_mode() do
  "read"
  |> get_mode()
end

def get_mode(mode_name) when is_map_key(@defs, mode_name) do
  struct(UserMode, @defs[mode_name])
  |> maybe_hydrate_component_selectors()
end

def get_mode(_) do
  get_initial_mode()
end
```

If used correctly, after page-load, the `%UserMode{}` will look like
this:

``` elixir
%Vyasa.Display.UserMode{
  mode: "read",
  default_ui_state: %Vyasa.Display.UiState{
    show_media_bridge?: true,
    show_action_bar?: true
  },
  mode_icon_name: "hero-book-open",
  quick_actions: [:mark_quote, :bookmark],
  control_panel_modes: ["discuss"],
  mode_actions: [:mark_quote, :bookmark],
  action_bar_actions: [:nav_back, :nav_fwd],
  action_bar_info_types: nil,
  action_bar_component: VyasaWeb.MediaLive.MediaBridge,
  action_bar_component_selector: "media-bridge",
  control_panel_component: VyasaWeb.ControlPanel,
  control_panel_component_selector: "control-panel",
  mode_context_component: VyasaWeb.Content.ReadingContent,
  mode_context_component_selector: "reading-content"
}
```

So, this is an example of where it gets used:

``` elixir
<.live_component
   module={@mode.mode_context_component}
   id={@mode.mode_context_component_selector}
   user_mode={@mode}
   url_params={@url_params}
   live_action={@live_action}
   session={@session}
/>
```

or

``` elixir
attr :user_mode, UserMode, required: true
@impl true
def render(assigns) do
  ~H"""
  <div
    id="hoverune"
    class="z-10 absolute hidden top-0 left-0 max-w-max group-hover:flex items-center bg-white/90 rounded-lg shadow-lg border border-gray-200 transition-all duration-300 ease-in-out p-1"
  >
    <%= for action <- @user_mode.quick_actions do %>
      <.hover_rune_quick_action
        action_event={get_quick_action_click_event(action)}
        action_icon_name={get_quick_action_icon_name(action)}
        action_target={@user_mode.mode_context_component_selector}
      />
    <% end %>
  </div>
  """
end
```
Co-authored-by: Ritesh Kumar <ritesh@emerald.pink>
Signed-off-by: a/vivekbala <avivekbala@gmail.com>
@ks0m1c ks0m1c closed this Sep 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants