···77 alias Bookmarker.Repo
88 alias Bookmarker.Bookmarks.Bookmark
99 alias Bookmarker.Tags
1010+ alias Bookmarker.Notifications
1011 alias Phoenix.PubSub
11121213 @topic "bookmarks"
···264265 # Broadcast to user's tag topics
265266 Enum.each(bookmark.tags, fn tag ->
266267 broadcast_to_user_tag(bookmark.user.username, tag.name, {:bookmark_created, bookmark})
268268+269269+ # Create notifications for users following this tag
270270+ Notifications.create_tag_bookmark_notifications(bookmark, tag)
267271 end)
268272269273 {:ok, bookmark}
···11+defmodule Bookmarker.NotificationsCache do
22+ @moduledoc """
33+ ETS-based cache for user notifications.
44+ """
55+66+ use GenServer
77+ alias Bookmarker.Notifications
88+ alias Bookmarker.Accounts
99+ alias Phoenix.PubSub
1010+1111+ @notifications_table :unread_notifications_cache
1212+1313+ # Client API
1414+1515+ def start_link(_opts) do
1616+ GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
1717+ end
1818+1919+ # Server callbacks
2020+2121+ @impl true
2222+ def init(:ok) do
2323+ # Create ETS table
2424+ :ets.new(@notifications_table, [:set, :named_table, :public, read_concurrency: true])
2525+2626+ # Initialize cache with data from the database
2727+ initialize_cache()
2828+2929+ # Subscribe to notification events
3030+ PubSub.subscribe(Bookmarker.PubSub, "tag_follows")
3131+3232+ # Schedule periodic cleanup of old notifications
3333+ schedule_cleanup()
3434+3535+ {:ok, %{}}
3636+ end
3737+3838+ @impl true
3939+ def handle_info({:tag_followed, tag_follow}, state) do
4040+ # We don't need to do anything special when a tag is followed
4141+ {:noreply, state}
4242+ end
4343+4444+ @impl true
4545+ def handle_info({:tag_unfollowed, _tag_follow}, state) do
4646+ # We don't need to do anything special when a tag is unfollowed
4747+ {:noreply, state}
4848+ end
4949+5050+ @impl true
5151+ def handle_info(:cleanup_notifications, state) do
5252+ # Truncate old notifications
5353+ Notifications.truncate_old_notifications()
5454+5555+ # Schedule the next cleanup
5656+ schedule_cleanup()
5757+5858+ {:noreply, state}
5959+ end
6060+6161+ @impl true
6262+ def handle_info(_, state), do: {:noreply, state}
6363+6464+ # Private functions
6565+6666+ defp initialize_cache do
6767+ # Get all users
6868+ users = Accounts.list_users()
6969+7070+ # For each user, load their unread notifications into the cache
7171+ Enum.each(users, fn user ->
7272+ unread_notifications = Notifications.list_unread_notifications(user)
7373+ :ets.insert(@notifications_table, {user.id, unread_notifications})
7474+ end)
7575+ end
7676+7777+ defp schedule_cleanup do
7878+ # Run cleanup once a day
7979+ Process.send_after(self(), :cleanup_notifications, 24 * 60 * 60 * 1000)
8080+ end
8181+end
···11+defmodule BookmarkerWeb.NotificationComponent do
22+ use BookmarkerWeb, :live_component
33+44+ alias Bookmarker.Notifications
55+ alias Bookmarker.Accounts.User
66+ alias Phoenix.PubSub
77+88+ @impl true
99+ def mount(socket) do
1010+ {:ok, socket}
1111+ end
1212+1313+ @impl true
1414+ def update(%{current_user: %User{} = user} = assigns, socket) do
1515+ # Subscribe to this user's notification events
1616+ if connected?(socket) do
1717+ PubSub.subscribe(Bookmarker.PubSub, "user:#{user.id}:notifications")
1818+ end
1919+2020+ unread_notifications = Notifications.get_unread_notifications(user)
2121+2222+ {:ok,
2323+ socket
2424+ |> assign(assigns)
2525+ |> assign(:unread_notifications, unread_notifications)
2626+ |> assign(:show_dropdown, false)}
2727+ end
2828+2929+ # This function will be called from the LiveView to handle messages for us
3030+ def handle_notification_message(id, notification) do
3131+ send_update(__MODULE__, id: id, new_notification: notification)
3232+ end
3333+3434+ # This is a special update function to handle messages sent via send_update
3535+ @impl true
3636+ def update(%{new_notification: notification} = _assigns, socket) do
3737+ updated_notifications = [notification | socket.assigns.unread_notifications]
3838+3939+ {:ok, assign(socket, :unread_notifications, updated_notifications)}
4040+ end
4141+4242+ @impl true
4343+ def handle_event("toggle-dropdown", _, socket) do
4444+ {:noreply, assign(socket, :show_dropdown, !socket.assigns.show_dropdown)}
4545+ end
4646+4747+ @impl true
4848+ def handle_event("mark-read", %{"id" => notification_id}, socket) do
4949+ notification = Enum.find(socket.assigns.unread_notifications, &(&1.id == String.to_integer(notification_id)))
5050+5151+ if notification do
5252+ Notifications.mark_notification_read(notification)
5353+ end
5454+5555+ {:noreply, socket}
5656+ end
5757+5858+ @impl true
5959+ def handle_event("mark-all-read", _, socket) do
6060+ Notifications.mark_all_notifications_read(socket.assigns.current_user)
6161+6262+ {:noreply, assign(socket, :unread_notifications, [])}
6363+ end
6464+6565+ @impl true
6666+ def handle_info({:new_notification, notification}, socket) do
6767+ updated_notifications = [notification | socket.assigns.unread_notifications]
6868+6969+ {:noreply, assign(socket, :unread_notifications, updated_notifications)}
7070+ end
7171+7272+ defp format_notification(%{notification_type: "new_bookmark_with_followed_tag"} = notification) do
7373+ "New bookmark '#{notification.data["bookmark_title"]}' with tag '#{notification.data["tag_name"]}'"
7474+ end
7575+end
···44 alias Bookmarker.Bookmarks
55 alias Bookmarker.Cache
66 alias Bookmarker.Accounts
77+ alias Bookmarker.Tags
88+ alias Bookmarker.Notifications
79810 @impl true
911 def mount(%{"tag_name" => tag_name}, _session, socket) do
···1719 # For demo purposes, let's get the first user or create one
1820 user = get_or_create_demo_user()
19212222+ # Get the tag record
2323+ tag = Tags.get_tag_by_name(tag_name)
2424+2525+ # Check if the user follows this tag
2626+ follows_tag = if user && tag, do: Notifications.follows_tag?(user, tag), else: false
2727+2028 {:ok,
2129 socket
2230 |> assign(:tag_name, tag_name)
3131+ |> assign(:tag, tag)
2332 |> assign(:bookmarks, bookmarks)
2433 |> assign(:current_user, user)
3434+ |> assign(:follows_tag, follows_tag)
2535 |> assign(:page_title, "Bookmarks tagged with #{tag_name}")}
2636 end
27372838 @impl true
3939+ def handle_event("follow_tag", _, socket) do
4040+ %{current_user: user, tag: tag} = socket.assigns
4141+4242+ case Notifications.follow_tag(user, tag) do
4343+ {:ok, _tag_follow} ->
4444+ {:noreply, assign(socket, :follows_tag, true)}
4545+ {:error, _changeset} ->
4646+ {:noreply, socket}
4747+ end
4848+ end
4949+5050+ @impl true
5151+ def handle_event("unfollow_tag", _, socket) do
5252+ %{current_user: user, tag: tag} = socket.assigns
5353+5454+ case Notifications.unfollow_tag(user, tag) do
5555+ {:ok, _tag_follow} ->
5656+ {:noreply, assign(socket, :follows_tag, false)}
5757+ {:error, _} ->
5858+ {:noreply, socket}
5959+ end
6060+ end
6161+6262+ @impl true
2963 def handle_info({:bookmark_created, bookmark}, socket) do
3064 # Check if the bookmark has the current tag
3165 if has_tag?(bookmark, socket.assigns.tag_name) do
···67101 |> Enum.reject(fn b -> b.id == bookmark.id end)
6810269103 {:noreply, assign(socket, :bookmarks, updated_bookmarks)}
104104+ end
105105+106106+ @impl true
107107+ def handle_info({:tag_followed, _tag_follow}, socket) do
108108+ {:noreply, assign(socket, :follows_tag, true)}
109109+ end
110110+111111+ @impl true
112112+ def handle_info({:tag_unfollowed, _tag_follow}, socket) do
113113+ {:noreply, assign(socket, :follows_tag, false)}
114114+ end
115115+116116+ @impl true
117117+ def handle_info({:new_notification, _notification}, socket) do
118118+ # We don't need to do anything with notifications in this LiveView
119119+ # They're handled by the notification component
120120+ {:noreply, socket}
70121 end
7112272123 defp has_tag?(bookmark, tag_name) do
···1414 plug :accepts, ["json"]
1515 end
16161717+ pipeline :rss do
1818+ plug :accepts, ["xml"]
1919+ plug :put_format, "xml"
2020+ end
2121+1722 scope "/", BookmarkerWeb do
1823 pipe_through :browser
1924···2530 live "/tags/:tag_name", TagLive.Show
2631 live "/users/:username", UserLive.Show
2732 live "/users/:username/tags/:tag_name", UserLive.TagShow
3333+ end
3434+3535+ scope "/", BookmarkerWeb do
3636+ pipe_through :rss
3737+3838+ get "/rss/tags/:tag_name", RssController, :tag
3939+ get "/rss/users/:username/tags/:tag_name", RssController, :user_tag
2840 end
29413042 # Other scopes may use custom stacks.